/**
 ******************************************************************************
 * Xenia : Xbox 360 Emulator Research Project                                 *
 ******************************************************************************
 * Copyright 2022 Ben Vanik. All rights reserved.                             *
 * Released under the BSD license - see LICENSE in the root for more details. *
 ******************************************************************************
 */

#ifndef XENIA_VFS_DEVICES_STFS_XBOX_H_
#define XENIA_VFS_DEVICES_STFS_XBOX_H_

#include <time.h>

#include "xenia/base/string_util.h"
#include "xenia/kernel/util/xex2_info.h"
#include "xenia/xbox.h"

namespace xe {
namespace vfs {

// Convert FAT timestamp to 100-nanosecond intervals since January 1, 1601 (UTC)
inline uint64_t decode_fat_timestamp(const uint32_t date, const uint32_t time) {
  struct tm tm = {0};
  // 80 is the difference between 1980 (FAT) and 1900 (tm);
  tm.tm_year = ((0xFE00 & date) >> 9) + 80;
  tm.tm_mon = ((0x01E0 & date) >> 5) - 1;
  tm.tm_mday = (0x001F & date) >> 0;
  tm.tm_hour = (0xF800 & time) >> 11;
  tm.tm_min = (0x07E0 & time) >> 5;
  tm.tm_sec = (0x001F & time) << 1;  // the value stored in 2-seconds intervals
  tm.tm_isdst = 0;

#if XE_PLATFORM_WIN32
  time_t timet = _mkgmtime(&tm);
#else
  time_t timet = timegm(&tm);
#endif

  if (timet == -1) {
    return 0;
  }
  // 11644473600LL is a difference between 1970 and 1601
  return (timet + 11644473600LL) * 10000000;
}

// Structs used for interchange between Xenia and actual Xbox360 kernel/XAM

inline uint32_t load_uint24_be(const uint8_t* p) {
  return (uint32_t(p[0]) << 16) | (uint32_t(p[1]) << 8) | uint32_t(p[2]);
}
inline uint32_t load_uint24_le(const uint8_t* p) {
  return (uint32_t(p[2]) << 16) | (uint32_t(p[1]) << 8) | uint32_t(p[0]);
}
inline void store_uint24_le(uint8_t* p, uint32_t value) {
  p[2] = uint8_t((value >> 16) & 0xFF);
  p[1] = uint8_t((value >> 8) & 0xFF);
  p[0] = uint8_t(value & 0xFF);
}

enum class XContentPackageType : uint32_t {
  kCon = 0x434F4E20,
  kPirs = 0x50495253,
  kLive = 0x4C495645,
};

enum class XContentVolumeType : uint32_t {
  kStfs = 0,
  kSvod = 1,
};

/* STFS structures */
#pragma pack(push, 1)
struct StfsVolumeDescriptor {
  uint8_t descriptor_length;
  uint8_t version;
  union {
    uint8_t as_byte;
    struct {
      uint8_t read_only_format : 1;  // if set, only uses a single backing-block
                                     // per hash table (no resiliency),
                                     // otherwise uses two
      uint8_t root_active_index : 1;  // if set, uses secondary backing-block
                                      // for the highest-level hash table

      uint8_t directory_overallocated : 1;
      uint8_t directory_index_bounds_valid : 1;
    } bits;
  } flags;
  uint16_t file_table_block_count;
  uint8_t file_table_block_number_raw[3];
  uint8_t top_hash_table_hash[0x14];
  be<uint32_t> total_block_count;
  be<uint32_t> free_block_count;

  uint32_t file_table_block_number() const {
    return load_uint24_le(file_table_block_number_raw);
  }

  void set_file_table_block_number(uint32_t value) {
    store_uint24_le(file_table_block_number_raw, value);
  }

  bool is_valid() const {
    return descriptor_length == sizeof(StfsVolumeDescriptor);
  }
};
static_assert_size(StfsVolumeDescriptor, 0x24);
#pragma pack(pop)

enum class StfsHashState : uint8_t {
  kFree = 0,   // unallocated but doesn't exist in package (needs to expand)?
  kFree2 = 1,  // unallocated but exists in package?
  kInUse = 2,
};

struct StfsHashEntry {
  uint8_t sha1[0x14];

  xe::be<uint32_t> info_raw;

  uint32_t level0_next_block() const { return info_raw & 0xFFFFFF; }
  void set_level0_next_block(uint32_t value) {
    info_raw = (info_raw & ~0xFFFFFF) | (value & 0xFFFFFF);
  }

  StfsHashState level0_allocation_state() const {
    return StfsHashState(uint8_t(((info_raw & 0xC0000000) >> 30) & 0xFF));
  }
  void set_level0_allocation_state(StfsHashState value) {
    info_raw = (info_raw & ~0xC0000000) | (uint32_t(value) << 30);
  }

  uint32_t levelN_num_blocks_free() const { return info_raw & 0x7FFF; }
  void set_levelN_num_blocks_free(uint32_t value) {
    info_raw = (info_raw & ~0x7FFF) | (value & 0x7FFF);
  }

  uint32_t levelN_num_blocks_unk() const {
    return ((info_raw & 0x3FFF8000) >> 15) & 0x7FFF;
  }
  void set_levelN_num_blocks_unk(uint32_t value) {
    info_raw = (info_raw & ~0x3FFF8000) | ((value & 0x7FFF) << 15);
  }

  bool levelN_active_index() const { return (info_raw & 0x40000000) != 0; }
  void set_levelN_active_index(bool value) {
    info_raw = (info_raw & ~0x40000000) | (value ? 0x40000000 : 0);
  }

  bool levelN_writeable() const { return (info_raw & 0x80000000) != 0; }
  void set_levelN_writeable(bool value) {
    info_raw = (info_raw & ~0x80000000) | (value ? 0x80000000 : 0);
  }
};
static_assert_size(StfsHashEntry, 0x18);

struct StfsHashTable {
  StfsHashEntry entries[170];
  xe::be<uint32_t> num_blocks;  // num L0 blocks covered by this table?
  uint8_t padding[12];
};
static_assert_size(StfsHashTable, 0x1000);

struct StfsDirectoryEntry {
  char name[40];

  struct {
    uint8_t name_length : 6;
    uint8_t contiguous : 1;
    uint8_t directory : 1;
  } flags;

  uint8_t valid_data_blocks_raw[3];
  uint8_t allocated_data_blocks_raw[3];
  uint8_t start_block_number_raw[3];

  be<uint16_t> directory_index;

  be<uint32_t> length;

  be<uint16_t> create_date;
  be<uint16_t> create_time;
  be<uint16_t> modified_date;
  be<uint16_t> modified_time;

  uint32_t valid_data_blocks() const {
    return load_uint24_le(valid_data_blocks_raw);
  }

  void set_valid_data_blocks(uint32_t value) {
    store_uint24_le(valid_data_blocks_raw, value);
  }

  uint32_t allocated_data_blocks() const {
    return load_uint24_le(allocated_data_blocks_raw);
  }

  void set_allocated_data_blocks(uint32_t value) {
    store_uint24_le(allocated_data_blocks_raw, value);
  }

  uint32_t start_block_number() const {
    return load_uint24_le(start_block_number_raw);
  }

  void set_start_block_number(uint32_t value) {
    store_uint24_le(start_block_number_raw, value);
  }
};
static_assert_size(StfsDirectoryEntry, 0x40);

struct StfsDirectoryBlock {
  StfsDirectoryEntry entries[0x40];
};
static_assert_size(StfsDirectoryBlock, 0x1000);

/* SVOD structures */
struct SvodDeviceDescriptor {
  uint8_t descriptor_length;
  uint8_t block_cache_element_count;
  uint8_t worker_thread_processor;
  uint8_t worker_thread_priority;
  uint8_t first_fragment_hash_entry[0x14];
  union {
    uint8_t as_byte;
    struct {
      uint8_t must_be_zero_for_future_usage : 6;
      uint8_t enhanced_gdf_layout : 1;
      uint8_t zero_for_downlevel_clients : 1;
    } bits;
  } features;
  uint8_t num_data_blocks_raw[3];
  uint8_t start_data_block_raw[3];
  uint8_t reserved[5];

  uint32_t num_data_blocks() { return load_uint24_le(num_data_blocks_raw); }

  uint32_t start_data_block() { return load_uint24_le(start_data_block_raw); }
};
static_assert_size(SvodDeviceDescriptor, 0x24);

/* XContent structures */
struct XContentLicense {
  be<uint64_t> licensee_id;
  be<uint32_t> license_bits;
  be<uint32_t> license_flags;
};
static_assert_size(XContentLicense, 0x10);

struct XContentMediaData {
  uint8_t series_id[0x10];
  uint8_t season_id[0x10];
  be<uint16_t> season_number;
  be<uint16_t> episode_number;
};
static_assert_size(XContentMediaData, 0x24);

struct XContentAvatarAssetData {
  be<uint32_t> sub_category;
  be<uint32_t> colorizable;
  uint8_t asset_id[0x10];
  uint8_t skeleton_version_mask;
  uint8_t reserved[0xB];
};
static_assert_size(XContentAvatarAssetData, 0x24);

struct XContentAttributes {
  uint8_t profile_transfer : 1;
  uint8_t device_transfer : 1;
  uint8_t move_only_transfer : 1;
  uint8_t kinect_enabled : 1;
  uint8_t disable_network_storage : 1;
  uint8_t deep_link_supported : 1;
  uint8_t reserved : 2;
};
static_assert_size(XContentAttributes, 1);

#pragma pack(push, 1)
struct XContentMetadata {
  static const uint32_t kThumbLengthV1 = 0x4000;
  static const uint32_t kThumbLengthV2 = 0x3D00;

  static const uint32_t kNumLanguagesV1 = 9;
  // metadata_version 2 adds 3 languages inside thumbnail/title_thumbnail space
  static const uint32_t kNumLanguagesV2 = 12;

  be<XContentType> content_type;
  be<uint32_t> metadata_version;
  be<uint64_t> content_size;
  xex2_opt_execution_info execution_info;
  uint8_t console_id[5];
  be<uint64_t> profile_id;
  union {
    StfsVolumeDescriptor stfs;
    SvodDeviceDescriptor svod;
  } volume_descriptor;
  be<uint32_t> data_file_count;
  be<uint64_t> data_file_size;
  be<XContentVolumeType> volume_type;
  be<uint64_t> online_creator;
  be<uint32_t> category;
  uint8_t reserved2[0x20];
  union {
    XContentMediaData media_data;
    XContentAvatarAssetData avatar_asset_data;
  } metadata_v2;
  uint8_t device_id[0x14];
  union {
    be<uint16_t> uint[kNumLanguagesV1][128];
    char16_t chars[kNumLanguagesV1][128];
  } display_name_raw;
  union {
    be<uint16_t> uint[kNumLanguagesV1][128];
    char16_t chars[kNumLanguagesV1][128];
  } description_raw;
  union {
    be<uint16_t> uint[64];
    char16_t chars[64];
  } publisher_raw;
  union {
    be<uint16_t> uint[64];
    char16_t chars[64];
  } title_name_raw;
  union {
    uint8_t as_byte;
    XContentAttributes bits;
  } flags;
  be<uint32_t> thumbnail_size;
  be<uint32_t> title_thumbnail_size;
  uint8_t thumbnail[kThumbLengthV2];
  union {
    be<uint16_t> uint[kNumLanguagesV2 - kNumLanguagesV1][128];
    char16_t chars[kNumLanguagesV2 - kNumLanguagesV1][128];
  } display_name_ex_raw;
  uint8_t title_thumbnail[kThumbLengthV2];
  union {
    be<uint16_t> uint[kNumLanguagesV2 - kNumLanguagesV1][128];
    char16_t chars[kNumLanguagesV2 - kNumLanguagesV1][128];
  } description_ex_raw;

  std::u16string display_name(XLanguage language) const {
    uint32_t lang_id = uint32_t(language) - 1;

    if (lang_id >= kNumLanguagesV2) {
      assert_always();
      // no room for this lang, read from english slot..
      lang_id = uint32_t(XLanguage::kEnglish) - 1;
    }

    const be<uint16_t>* str = 0;
    if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
      str = display_name_raw.uint[lang_id];
    } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
               metadata_version >= 2) {
      str = display_name_ex_raw.uint[lang_id - kNumLanguagesV1];
    }

    if (!str) {
      // Invalid language ID?
      assert_always();
      return u"";
    }

    return load_and_swap<std::u16string>(str);
  }

  std::u16string description(XLanguage language) const {
    uint32_t lang_id = uint32_t(language) - 1;

    if (lang_id >= kNumLanguagesV2) {
      assert_always();
      // no room for this lang, read from english slot..
      lang_id = uint32_t(XLanguage::kEnglish) - 1;
    }

    const be<uint16_t>* str = 0;
    if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
      str = description_raw.uint[lang_id];
    } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
               metadata_version >= 2) {
      str = description_ex_raw.uint[lang_id - kNumLanguagesV1];
    }

    if (!str) {
      // Invalid language ID?
      assert_always();
      return u"";
    }

    return load_and_swap<std::u16string>(str);
  }

  std::u16string publisher() const {
    return load_and_swap<std::u16string>(publisher_raw.uint);
  }

  std::u16string title_name() const {
    return load_and_swap<std::u16string>(title_name_raw.uint);
  }

  bool set_display_name(XLanguage language, const std::u16string_view value) {
    uint32_t lang_id = uint32_t(language) - 1;

    if (lang_id >= kNumLanguagesV2) {
      assert_always();
      // no room for this lang, store in english slot..
      lang_id = uint32_t(XLanguage::kEnglish) - 1;
    }

    char16_t* str = 0;
    if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
      str = display_name_raw.chars[lang_id];
    } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
               metadata_version >= 2) {
      str = display_name_ex_raw.chars[lang_id - kNumLanguagesV1];
    }

    if (!str) {
      // Invalid language ID?
      assert_always();
      return false;
    }

    string_util::copy_and_swap_truncating(str, value,
                                          countof(display_name_raw.chars[0]));
    return true;
  }

  bool set_description(XLanguage language, const std::u16string_view value) {
    uint32_t lang_id = uint32_t(language) - 1;

    if (lang_id >= kNumLanguagesV2) {
      assert_always();
      // no room for this lang, store in english slot..
      lang_id = uint32_t(XLanguage::kEnglish) - 1;
    }

    char16_t* str = 0;
    if (lang_id >= 0 && lang_id < kNumLanguagesV1) {
      str = description_raw.chars[lang_id];
    } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 &&
               metadata_version >= 2) {
      str = description_ex_raw.chars[lang_id - kNumLanguagesV1];
    }

    if (!str) {
      // Invalid language ID?
      assert_always();
      return false;
    }

    string_util::copy_and_swap_truncating(str, value,
                                          countof(description_raw.chars[0]));
    return true;
  }

  void set_publisher(const std::u16string_view value) {
    string_util::copy_and_swap_truncating(publisher_raw.chars, value,
                                          countof(publisher_raw.chars));
  }

  void set_title_name(const std::u16string_view value) {
    string_util::copy_and_swap_truncating(title_name_raw.chars, value,
                                          countof(title_name_raw.chars));
  }
};
static_assert_size(XContentMetadata, 0x93D6);

struct XContentHeader {
  be<XContentPackageType> magic;
  uint8_t signature[0x228];
  XContentLicense licenses[0x10];
  uint8_t content_id[0x14];
  be<uint32_t> header_size;

  bool is_magic_valid() const {
    return magic == XContentPackageType::kCon ||
           magic == XContentPackageType::kLive ||
           magic == XContentPackageType::kPirs;
  }
};
static_assert_size(XContentHeader, 0x344);
#pragma pack(pop)

struct XContentContainerHeader {
  XContentHeader content_header;
  XContentMetadata content_metadata;
  // TODO: title/system updates contain more data after XContentMetadata, seems
  // to affect header.header_size

  bool is_package_readonly() const {
    if (content_metadata.volume_type == vfs::XContentVolumeType::kSvod) {
      return true;
    }

    return content_metadata.volume_descriptor.stfs.flags.bits.read_only_format;
  }
};
static_assert_size(XContentContainerHeader, 0x971A);

}  // namespace vfs
}  // namespace xe

#endif  // XENIA_VFS_DEVICES_STFS_XBOX_H_
