use modular_bitfield::prelude::*;
use crate::Hash40;

use binread::{
    BinRead,
    derive_binread,
    ReadOptions,
    io::*,
    BinResult
};

#[derive(BinRead, Debug, Clone, Copy)]
#[br(magic = 0x10_u32)]
pub struct CompTableHeader {
    pub decomp_size: u32,
    pub comp_size: u32,
    pub section_size: u32,
}

pub(crate) struct CompressedFileSystem(pub FileSystem);

impl BinRead for CompressedFileSystem {
    type Args = ();

    fn read_options<R>(reader: &mut R, options: &ReadOptions, args: Self::Args) -> BinResult<Self>
        where R: Read + Seek,
    {
        let header = CompTableHeader::read_options(reader, options, args)?;

        let mut compressed = vec![0; header.comp_size as usize];

        reader.read_exact(&mut compressed)?;

        let compressed = Cursor::new(compressed);
        let mut decompressed = Cursor::new(crate::zstd_backend::decode_all(compressed)?);

        FileSystem::read_options(&mut decompressed, options, args)
            .map(CompressedFileSystem)
    }
}

/// The filesystem itself. Includes all the linking between paths, file data, directories, and
/// mass-loading groups.
#[derive_binread]
#[derive(Debug)]
pub struct FileSystem {
    pub fs_header: FileSystemHeader,

    #[br(align_before = 0x100)]
    pub stream_header: StreamHeader,

    #[br(count = stream_header.quick_dir_count)]
    pub quick_dirs: Vec<QuickDir>,

    #[br(count = stream_header.stream_hash_count)]
    pub stream_hash_to_entries: Vec<HashToIndex>,

    #[br(count = stream_header.stream_hash_count)]
    pub stream_entries: Vec<StreamEntry>,
    
    #[br(count = stream_header.stream_file_index_count)]
    pub stream_file_indices: Vec<u32>,
    
    #[br(count = stream_header.stream_offset_entry_count)]
    pub stream_datas: Vec<StreamData>,

    #[br(temp)]
    pub hash_index_group_count: u32,
    
    #[br(temp)]
    pub bucket_count: u32,

    #[br(count = bucket_count)]
    pub file_info_buckets: Vec<FileInfoBucket>,

    #[br(count = hash_index_group_count)]
    pub file_hash_to_path_index: Vec<HashToIndex>,

    #[br(count = fs_header.file_info_path_count)]
    pub file_paths: Vec<FilePath>,

    #[br(count = fs_header.file_info_index_count)]
    pub file_info_indices: Vec<FileInfoIndex>,
    
    #[br(count = fs_header.folder_count)]
    pub dir_hash_to_info_index: Vec<HashToIndex>,

    #[br(count = fs_header.folder_count)]
    pub dir_infos: Vec<DirInfo>,
    
    #[br(count = fs_header.folder_offset_count_1 + fs_header.folder_offset_count_2 + fs_header.extra_folder)]
    pub folder_offsets: Vec<DirectoryOffset>,

    #[br(count = fs_header.hash_folder_count)]
    pub folder_child_hashes: Vec<HashToIndex>,

    #[br(count = fs_header.file_info_count + fs_header.sub_file_count_2 + fs_header.extra_count)]
    pub file_infos: Vec<FileInfo>,

    #[br(count = fs_header.file_info_sub_index_count + fs_header.sub_file_count_2 + fs_header.extra_count_2)]
    pub file_info_to_datas: Vec<FileInfoToFileData>,

    #[br(count = fs_header.sub_file_count + fs_header.sub_file_count_2 + fs_header.extra_count)]
    pub file_datas: Vec<FileData>,
}

/// Header to the filesystem, primarily just various counts
#[derive(BinRead, Debug, Clone, Copy)]
pub struct FileSystemHeader {
    pub table_filesize: u32,
    pub file_info_path_count: u32,
    pub file_info_index_count: u32,
    pub folder_count: u32,

    pub folder_offset_count_1: u32,

    pub hash_folder_count: u32,
    pub file_info_count: u32,
    pub file_info_sub_index_count: u32,
    pub sub_file_count: u32,

    pub folder_offset_count_2: u32,
    pub sub_file_count_2: u32,
    pub padding: u32,

    pub unk1_10: u32, // always 0x10
    pub unk2_10: u32, // always 0x10

    pub regional_count_1: u8,
    pub regional_count_2: u8,
    pub padding2: u16,
    
    pub version: u32,
    pub extra_folder: u32,
    pub extra_count: u32,

    pub unk: [u32; 2],

    pub extra_count_2: u32,
    pub extra_sub_count: u32,
}

/// Header for the sizes of various stream lists in the filesystem
#[derive(BinRead, Debug)]
pub struct StreamHeader {
    pub quick_dir_count: u32,
    pub stream_hash_count: u32,
    pub stream_file_index_count: u32,
    pub stream_offset_entry_count: u32,
}

/// Directory listing for stream files
#[bitfield]
#[derive(BinRead, Debug, Clone, Copy)]
#[br(map = Self::from_bytes)]
pub struct QuickDir {
    pub hash: u32,
    pub name_length: u8,
    pub count: B24,
    pub index: u32,
}

/// An entry representing a single stream file
#[bitfield]
#[derive(BinRead, Debug, Clone, Copy)]
#[br(map = Self::from_bytes)]
pub struct StreamEntry {
    pub hash: u32,
    pub name_length: u8,
    pub index: B24,
    pub flags: u32,
}

/// A mapping from a hash to an index into another list, used for the hash lookup tables in the
/// format
#[bitfield]
#[derive(BinRead, Debug, Clone, Copy)]
#[br(map = Self::from_bytes)]
pub struct HashToIndex {
    pub hash: u32,
    pub length: u8,
    pub index: B24,
}

/// A range within the file info list
#[derive(BinRead, Debug, Clone, Copy)]
pub struct FileInfoBucket {
    pub start: u32,
    pub count: u32,
}

/// A set of hashes representing the components of a path
///
/// Note: since each component is a hash, a `HashLabels` object is needed to recover the string
/// forms of the components
#[derive(BinRead, Debug, Clone, Copy)]
pub struct FilePath {
    /// Hash of the full absolute path
    ///
    /// For example, the path "fighter/mario/c00/model.nutexb" would have a `path` of
    /// "fighter/mario/c00/model.nutexb"
    pub path: HashToIndex,
    /// Hash of the file extension
    ///
    /// For example, the path "fighter/mario/c00/model.nutexb" would have an extension of "nutexb"
    pub ext: HashToIndex,
    /// Hash of the absolute path of the parent directory
    /// 
    /// For example, the path "fighter/mario/c00/model.nutexb" would have a parent of
    /// "fighter/mario/c00" (or "fighter/mario/c00/")
    pub parent: HashToIndex,
    /// Hash of the name of the file relative to the parent directory
    ///
    /// For example, the path "fighter/mario/c00/model.nutexb" would have a filename of
    /// "model.nutexb"
    pub file_name: HashToIndex,
}

/// A collection of indices representing a pair of [`DirectoryOffset`] and [`FileInfo`] as
/// retrieved by index. These can be used together to retrieve the actual file itself in various
/// ways (either by loading the full directory or the individual file).
#[derive(BinRead, Debug, Clone, Copy)]
pub struct FileInfoIndex {
    pub dir_offset_index: u32,
    pub file_info_index: u32,
}

/// Various info about a directory. This can either represent a "real" directory, or merely a
/// mass-loading group.
#[derive(BinRead, Debug, Clone)]
pub struct DirInfo {
    /// A crc32 of the path of the directory
    pub path_hash: u32,
    pub dir_offset_index: u32,
    /// The name of the directory, relative to the parent
    pub name: Hash40,
    /// The parent directory this directory is within
    pub parent: Hash40,
    pub extra_dis_re: u32,
    pub extra_dis_re_length: u32,
    pub file_name_start_index: u32,
    pub file_info_count: u32,
    pub child_dir_start_index: u32,
    pub child_dir_count: u32,
    pub flags: u32,
}

/// A range of offsets within the data.arc relative to the start of the
/// `ArcFile::stream_section_offset`. This data is never compressed.
#[derive(BinRead, Debug, Clone, Copy)]
pub struct StreamData {
    pub size: u64,
    pub offset: u64,
}

/// Represents the location of a directory, both within the [`FileSystem::file_info_indices`] and
/// within the data.arc relative to [`ArcFile::file_section_offset`]
#[derive(BinRead, Debug, Clone)]
pub struct DirectoryOffset {
    pub offset: u64,
    pub decomp_size: u32,
    pub size: u32,
    pub sub_data_start_index: u32,
    pub sub_data_count: u32,
    pub resource_index: u32,
}

/// Information about a given file
#[derive(BinRead, Debug, Clone, Copy)]
pub struct FileInfo {
    // In CrossArc: PathIndex
    pub hash_index: u32,
    // In CrossArc: IndexIndex
    pub hash_index_2: u32,
    // In CrossArc: SubIndexIndex
    pub info_to_data_index: u32,
    // In CrossArc: Flags
    pub flags: FileInfoFlags,
}

/// Flags related to a given [`FileInfo`]
#[bitfield]
#[derive(BinRead, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[br(map = Self::from_bytes)]
pub struct FileInfoFlags {
    pub unused: B4,
    pub is_redirect: bool,
    pub unused2: B7,
    pub unknown1: bool,
    pub padding3: B2,
    pub is_regional: bool,
    pub is_localized: bool,
    pub unused3: B3,
    pub unknown2: bool,
    pub unknown3: bool,
    pub unused4: B10,
}

/// An entry in a map between [`FileInfo`]s and [`FileData`]s
#[derive(BinRead, Debug, Clone, Copy)]
pub struct FileInfoToFileData {
    pub folder_offset_index: u32,
    pub file_data_index: u32,
    pub file_info_index_and_flag: u32,
}

/// The data backing any number of files
#[repr(C)]
#[derive(BinRead, Debug, Clone, Copy)]
pub struct FileData {
    pub offset_in_folder: u32,
    /// Compressed size of file
    pub comp_size: u32,
    /// Decompressed size of file
    pub decomp_size: u32,
    /// Flags indicating the type of compression, if any, to use
    pub flags: FileDataFlags,
}

/// Flags indicating the type of compression, if any, to use
#[bitfield]
#[derive(BinRead, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[br(map = Self::from_bytes)]
pub struct FileDataFlags {
    pub compressed: bool,
    pub use_zstd: bool,
    pub unk: B30,
}
