pub (super) mod helper;
pub mod layout;
pub mod file_map;
pub mod overlay;

use cdtypes::types::{cdstring::DString, dir_record::DirectoryRecord, helper::xa_audio_interleave_count, path_table::PathTableL, sector::AudioSample};
use std::{cell::RefCell, path::PathBuf, rc::Rc};

pub use file_map::FileSystemMap;
pub use tool_helper::Error;

use crate::{encoder::cd, types::helper::InterleavedXASizes};
pub type SharedPtr<T> = Rc<RefCell<T>>;
pub type RawData      = Vec<u8>;

pub fn new_shared_ptr<T>(value: T) -> SharedPtr<T> {
    Rc::new(RefCell::new(value))
}

pub struct CDDATrack {
    pub(super) samples: Vec<AudioSample>,
    pub(super) align:   bool,
}

impl CDDATrack {
    pub fn new(samples: Vec<AudioSample>, align: bool) -> CDDATrack {
        CDDATrack{samples, align}
    }
}

pub struct CDDesc {
    pub(super) system_area:       SharedPtr<SystemArea>,
    pub(super) path_table:        SharedPtr<PathTable>,
    pub(super) pvd:               SharedPtr<PrimaryVolumeDescriptor>,
    pub(super) root:              SharedPtr<Directory>,
    pub(super) cd_da_tracks:      Vec<CDDATrack>,
    pub(super) vol_sector_count:  usize,
    pub        lead_out_sectors:  usize,
    pub        end_dummy_padding: usize,
}

impl CDDesc {
    pub fn new() -> CDDesc {
        match Directory::new("\x00") {
            Ok(root)   => CDDesc{
                system_area:       new_shared_ptr(SystemArea::new()), 
                path_table:        new_shared_ptr(PathTable::new()), 
                pvd:               new_shared_ptr(PrimaryVolumeDescriptor::new()), 
                root:              new_shared_ptr(root),
                cd_da_tracks:      Vec::new(),
                vol_sector_count:  0,
                lead_out_sectors:  0,
                end_dummy_padding: 0
            },
            Err(error) => panic!("Creating root directory failed with: {}", error)
        }
    }

    pub fn get_memory_layout(&self) -> layout::MemoryLayout {
        layout::DefaultLayout::new(self)
    }

    pub fn add_dir(&mut self, dir: Directory) {
        self.root.borrow_mut().add_dir(dir);
    }

    pub fn add_file(&mut self, file: File) {
        self.root.borrow_mut().add_file(file);
    }

    pub fn create_file_map(&self) -> FileSystemMap {
        file_map::new_file_map(&self.root.borrow())
    }

    pub fn get_content_size(&self) -> usize {
        let mut size = 0;

        Self::for_each_dir(&self.root, &mut |dir| {
            size += dir.get_extended_size();
            for file in &dir.files {
                size += file.borrow().get_extended_size();
            }
        });
        
        size += cd::size_of::lead_out(self.lead_out_sectors).full_size.bytes.unwrap_or(0);
        size
    }

    pub(super) fn for_each_dir_mut<T: Fn(&mut Directory)>(dir: SharedPtr<Directory>, function: &T) {
        let mut dir = dir.borrow_mut();

        function(&mut dir);
        for sub_dir in &mut dir.dirs {
            Self::for_each_dir_mut(sub_dir.clone(), function);
        }
    }

    fn for_each_dir<T: FnMut(&Directory)>(dir: &SharedPtr<Directory>, function: &mut T) {
        let dir = dir.borrow();

        function(&dir);
        for sub_dir in &dir.dirs {
            Self::for_each_dir(&sub_dir, function);
        }
    }
}

pub enum LicenseFile {
    None,
    Authentic(PathBuf),
    TMD(PathBuf, Option<String>),
}

impl LicenseFile {
    pub fn from_authentic_option(option: Option<PathBuf>) -> LicenseFile {
        if let Some(path) = option {
            LicenseFile::Authentic(path)
        }

        else {
            LicenseFile::None
        }
    }

    pub fn from_tmd_option(option: Option<PathBuf>, lic_text: Option<&str>) -> LicenseFile {
        if let Some(path) = option {
            LicenseFile::TMD(path, {
                if let Some(lic_text) = lic_text {
                    Some(lic_text.to_owned())
                }
                else {
                    None
                }
            })
        }

        else {
            LicenseFile::None
        }
    }
}

pub struct SystemArea {
    pub(in super) lba:                 LBA,
    pub(in super) license_file:        LicenseFile,
    pub(in super) backup_license_file: LicenseFile,
}

impl SystemArea {
    pub fn new() -> SystemArea {
        SystemArea{lba: LBA::default(), license_file: LicenseFile::None, backup_license_file: LicenseFile::None}
    }
}

pub struct PathTable {
    pub(super) lba:        LBA,
    pub(super) size_bytes: usize,
}

impl PathTable {
    pub fn new() -> PathTable {
        PathTable{lba: LBA::default(), size_bytes: 0}
    }

    pub fn collect_member(root: &Directory) -> Vec<helper::PathTableMember> {
        helper::collect_path_table_member(root)
    }

    pub fn calculate_size_for(root: &Directory) -> usize {
        let mut size_bytes = 0;

        Self::collect_member(root).into_iter().for_each(|element| {
            size_bytes += PathTableL::calculate_size_for(element.name.as_ref());
        });

        size_bytes
    }

    pub fn get_track_rel_lba_for(&self, table_num: usize, sector_count_func: fn(data_size: usize) -> usize) -> usize {
        let table_num = table_num - 1;
        self.lba.get_track_relative() + (table_num*sector_count_func(self.size_bytes))
    }
}

pub struct PrimaryVolumeDescriptor {
    pub(super) lba:       LBA,
    pub(super) publisher: String,
}

impl PrimaryVolumeDescriptor {
    pub fn new() -> PrimaryVolumeDescriptor {
        PrimaryVolumeDescriptor{lba: LBA::default(), publisher: String::new()}
    }

    pub fn set_publisher(&mut self, publisher: String) {
        self.publisher = publisher.to_uppercase();
    }
}

pub struct Directory {
    pub name:              DirectoryName,
    pub properties:        SharedPtr<Properties>,
        parent_properties: Option<SharedPtr<Properties>>,
        files:             Vec<SharedPtr<File>>,
        dirs:              Vec<SharedPtr<Directory>>
}

impl Directory {
    pub fn new(dir_name: &str) -> Result<Directory, Error> {
        Ok(Directory{name: DirectoryName::from_str(dir_name)?, properties: new_shared_ptr(Properties::default()), parent_properties: None, files: Vec::new(), dirs: Vec::new()})
    }

    pub fn add_dir(&mut self, mut dir: Directory) {
        dir.parent_properties = Some(self.properties.clone());
        self.dirs.push(new_shared_ptr(dir));
    }

    pub fn add_file(&mut self, mut file: File) -> SharedPtr<File> {
        file.parent_properties = Some(self.properties.clone());

        let file = new_shared_ptr(file);
        self.files.push(file.clone());
        file
    }

    pub fn get_track_rel_lba(&self) -> usize {
        self.properties.borrow().lba.get_track_relative()
    }

    pub fn get_absolute_lba(&self) -> usize {
        self.properties.borrow().lba.get_track_absolute()
    }

    pub fn get_extended_size(&self) -> usize {
        self.properties.borrow().get_padded_size()
    }

    pub(super) fn collect_member(&self) -> Vec<helper::DirectoryRecordMember>{
        helper::collect_directory_record_member(&self)
    }

    pub(super) fn update_content_size(&mut self) {
        let mut size_bytes = 0;

        for member in self.collect_member() {
            size_bytes += DirectoryRecord::calculate_size_for(member.get_name().as_str(), true);
        }

        self.properties.borrow_mut().size_bytes = size_bytes;
    }

    pub(super) fn file_iter(&self) -> core::slice::Iter<SharedPtr<File>> {
        self.files.iter()
    }

    pub(super) fn dir_iter(&self) -> core::slice::Iter<SharedPtr<Directory>> {
        self.dirs.iter()
    }
}

impl std::fmt::Display for Directory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Directory: {}", self.name)
    }    
}

pub(super) enum FileType {
    Regular(RawData),
    Main(RawData, overlay::LBANameVec),
    Overlay(RawData, overlay::LBANameVec),
    XAAudio(Vec<RawData>),
}

pub struct File {
    pub        name:              FileName,
    pub        properties:        Properties,
               parent_properties: Option<SharedPtr<Properties>>,
    pub(super) content:           FileType
}

impl File {
    pub fn new_regular(file_name: &str, content: RawData) -> Result<File, Error> {
        let content_size = content.len();

        Self::new_from_content(file_name, FileType::Regular(content), content_size)
    }

    pub fn new_main(file_name: &str, content: RawData, lba_names: overlay::LBANameVec) -> Result<File, Error> {
        let content_size = content.len();

        Self::new_from_content(file_name, FileType::Main(content, lba_names), content_size)
    }

    pub fn new_overlay(file_name: &str, content: RawData, lba_names: overlay::LBANameVec, content_size: usize) -> Result<File, Error> {
        Self::new_from_content(file_name, FileType::Overlay(content, lba_names), content_size)
    }

    pub fn new_xa_audio(file_name: &str, content: Vec<RawData>) -> Result<File, Error> {
        fn increase_content(mut content: Vec<RawData>) -> Vec<RawData> {
            let channel_count    = content.len();
            let xa_channel_count = xa_audio_interleave_count(channel_count);
            if channel_count < xa_channel_count {
                for _ in 0..(xa_channel_count - channel_count) {
                    content.push(RawData::new());
                }
            }
            content
        }
        let content      = increase_content(content);
        let content_size = InterleavedXASizes::new(&content);

        Self::new_from_content(file_name, FileType::XAAudio(content), content_size.full_size_bytes)
    }

    pub fn is_streaming_file(&self) -> bool {
        match &self.content {
            FileType::XAAudio(_) => true,
            _                    => false
        }
    }

    pub fn get_track_rel_lba(&self) -> usize {
        self.properties.lba.get_track_relative()
    }

    pub fn get_absolute_lba(&self) -> usize {
        self.properties.lba.get_track_absolute()
    }

    pub fn get_extended_size(&self) -> usize {
        self.properties.get_padded_size()
    }

    pub(super) fn make_regular(&mut self, content: RawData) {
        self.properties.size_bytes = content.len();
        self.content               = FileType::Regular(content);
    }

    fn new_from_content(file_name: &str, content: FileType, content_size: usize) -> Result<File, Error> {
        let mut file = File{name: FileName::from_str(file_name)?, properties: Properties::default(), parent_properties: None, content: content};

        file.properties.size_bytes = content_size;
        Ok(file)
    }
}

impl std::fmt::Display for File {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "File: {}", self.name)
    }
}

pub struct DirectoryName {
    name: DString<8>,
    len:  usize,
}

impl DirectoryName {
    pub fn from_str(dir_name: &str) -> Result<DirectoryName, Error> {
        let dir_name = dir_name.to_uppercase();
        Ok(DirectoryName{name: DString::from_str(dir_name.as_ref())?, len: dir_name.len()})
    }

    pub fn as_string(&self) -> Option<String> {
        Some(self.as_str()?.to_owned())
    }

    pub fn as_str(&self) -> Option<&str> {
        dstring_as_str(&self.name, self.len)
    }

    pub fn is_root(&self) -> bool {
        self.name.as_raw()[0] == 0
    }
}

impl std::fmt::Display for DirectoryName {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str().unwrap_or("<???>"))
    } 
}

pub struct FileName {
    name:    DString<8>,
    len:     usize,
    ext:     Option<DString<3>>,
    ext_len: Option<usize>
}

impl FileName {
    pub fn from_str(file_name: &str) -> Result<FileName, Error> {
        let file_name = file_name.to_uppercase();
        let (name, ext) = {
            let mut sub_str = file_name.split('.');
            (Error::ok_or_new(sub_str.next(), || "File name can't be empty".to_owned())?, sub_str.next())
        };
        let (ext, ext_len) = {
            match ext {
                Some(ext) => (Some(DString::from_str(ext)?), Some(ext.len())),
                None      => (None, None)
            }   
        };

        Ok(FileName{name: DString::from_str(name)?, len: name.len(), ext, ext_len})
    }

    pub fn as_string(&self) -> Option<String> {
        if let Some(ext) = &self.ext {
            Some(format!("{}.{};1", dstring_as_str(&self.name, self.len)?, dstring_as_str(&ext, self.ext_len.unwrap_or(0))?))
        }

        else {
            Some(format!("{}", dstring_as_str(&self.name, self.len)?))
        }
    }
}

impl std::fmt::Display for FileName {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_string().unwrap_or("<???>".to_owned()))
    }
}

pub struct Properties {
    pub(super) lba:               LBA,
    pub(super) size_bytes:        usize,
    pub(super) padded_size_bytes: Option<usize>,
    pub(super) is_hidden:         bool,
    pub(super) is_lz4:            bool,
}

impl Properties {
    pub(super) fn get_padded_size(&self) -> usize {
        if let Some(forced_bytes) = self.padded_size_bytes {
            forced_bytes
        }

        else {
            self.size_bytes
        }    
    }

    pub(super) fn get_real_size(&self) -> usize {
        self.size_bytes
    }

    pub fn is_size_valid(&self) -> bool {
        self.get_padded_size() >= self.size_bytes
    }
}

impl Default for Properties {
    fn default() -> Self {
        Properties{lba: LBA::default(), size_bytes: 0, padded_size_bytes: None, is_hidden: false, is_lz4: false}
    }
}

fn dstring_as_str<const SIZE: usize>(string: &DString<SIZE>, len: usize) -> Option<&str> {
    match std::str::from_utf8(&string.as_raw()[0..len]) {
        Ok(str) => Some(str),
        Err(_)  => None,
    }
}

pub struct LBA {
    track_offset: usize,
    track_rel:    usize
}

impl LBA {
    pub const FIRST_TRACK_OFFSET:usize = cdtypes::types::time::Time::cd_start().to_lba();

    pub fn set_track_offset(&mut self, offset: usize) {
        self.track_offset = offset;
    }

    pub fn set_track_rel(&mut self, new_track_rel: usize) {
        self.track_rel = new_track_rel;
    }

    pub fn overwrite(&mut self, rel_lba: usize, offset: usize) {
        self.set_track_offset(offset);
        self.set_track_rel(rel_lba);
    }

    pub fn get_track_relative(&self) -> usize {
        self.track_rel
    }

    pub fn get_track_absolute(&self) -> usize {
        self.track_rel + self.track_offset
    }
}

impl std::default::Default for LBA {
    fn default() -> Self {
        LBA{track_offset: 0, track_rel: 0}
    }
}