use super::RawData;
use super::{layout::Layout, File, FileSystemMap};
use super::super::encoder::cd::calculate_length_for;
use std::path::PathBuf;
use no_comment::{IntoWithoutComments as _, languages};
use tool_helper::{bits::{Bit, BitRange}, Error, format_if_error, read_file, read_file_to_string, format_if_error_drop_cause};

pub type LBANameVec = Vec<String>;

mod main;

#[repr(packed)]
struct LBAEntry {
    raw: u32,
}

impl LBAEntry {
    const SIZE_IN_SECTOR_RANGE:BitRange = BitRange::from_to(22, 31);
    const IS_LZ4_COMPRESSED:Bit         = Bit::at(19);
    const LBA_VALUE_RANGE:BitRange      = BitRange::from_to(0,  18);

    pub fn write_entry(&mut self, lba: usize, size_bytes: usize, is_lz4: bool) -> Result<(), Error> {
        if lba > Self::LBA_VALUE_RANGE.max_value() {
            return Err(Error::from_text(format!("LBA of value {} is impossible and can not be encoded! Maximum LBA value is: {}", lba, Self::LBA_VALUE_RANGE.max_value())));
        }

        let size_in_sectors = cdtypes::types::helper::sector_count_mode2_form1(size_bytes);
        if size_in_sectors > Self::SIZE_IN_SECTOR_RANGE.max_value() {
            return Err(Error::from_text(format!("{} sectors can not be encoded into 10bit", size_in_sectors)));
        }

        let lba             = usize::from_ne_bytes(lba.to_le_bytes());
        let size_in_sectors = usize::from_ne_bytes(size_in_sectors.to_le_bytes());

        if self.raw != 0 {
            return Err(Error::from_str("LBA Entry will overwrite non-zero value!\nIs no space allocated for the LBA Entries?"));
        }

        let new_raw = Self::LBA_VALUE_RANGE.or_value(0, lba);
        let new_raw = Self::SIZE_IN_SECTOR_RANGE.or_value(new_raw, size_in_sectors);
        let new_raw = Self::IS_LZ4_COMPRESSED.or_value(new_raw, is_lz4);

        self.raw = new_raw as u32;
        Ok(())
    }
}

pub fn load_from(file_name: &str, file_path: &PathBuf, lba_source: PathBuf) -> Result<File, Error> {
    let content      = read_file(file_path)?;
    let lba_names    = load_lba_names(lba_source)?;
    let content_size = format_if_error!(tool_helper::compress::psx_default::lz4(&content), "Compressing {} failed with \"{error_text}\"", file_path.to_string_lossy())?.len();
    
    Ok(File::new_overlay(file_name, content, lba_names, content_size)?)
}

pub fn load_for_main(file_name: &str, content: RawData, lba_source: PathBuf) -> Result<File, Error> {
    File::new_main(file_name, content, load_lba_names(lba_source)?)
}

pub fn update_content(content: &mut RawData, lba_names: &LBANameVec, file_map: &FileSystemMap) -> Result<RawData, Error> {
    let lba_header = skip_to_lba_header(content);
    let lba_header = unsafe{std::slice::from_raw_parts_mut(lba_header.as_mut_ptr() as *mut LBAEntry, lba_names.len())};
    
    for_each_lba_name(lba_names, file_map, |idx, (lba, bytes), is_lz4| {
        lba_header[idx].write_entry(lba, bytes, is_lz4)
    })?;

    Ok(tool_helper::compress::psx_default::lz4(content)?)
}

pub fn update_content_for_main(content: &mut RawData, lba_names: &LBANameVec, file_map: &FileSystemMap) -> Result<RawData, Error> {
    let lba_header = unsafe{std::slice::from_raw_parts_mut(main::skip_to_lba_area(content).as_mut_ptr() as *mut LBAEntry, lba_names.len())};
    
    for_each_lba_name(lba_names, file_map, |idx, (lba, bytes), is_lz4| {
        lba_header[idx].write_entry(lba, bytes, is_lz4)
    })?;

    Ok(content.clone())
}

fn for_each_lba_name<F: FnMut(usize, (usize, usize), bool) -> Result<(), Error>>(lba_names: &LBANameVec, file_map: &FileSystemMap, mut functor: F) -> Result<(), Error> {
    let mut idx = 0;
    for lba_name in lba_names {
        if let Some(file) = file_map.get(lba_name) {
            let file_ref     = format_if_error_drop_cause!(file.try_borrow(), "Failed accessing file \"{}\" for writing LBA information.\nNote: You can not inject the LBA information of a file into itself.", lba_name)?;
            let (lba, bytes) = (file_ref.get_absolute_lba(), {
                if file_ref.is_streaming_file() {
                    // Streaming files do not have a size restriction
                    Some(0)
                }

                else {
                    calculate_length_for(&Layout::File(file.clone())).full_size.bytes
                }
            });
            let is_lz4       = file_ref.properties.is_lz4;

            functor(idx, (lba, bytes.ok_or(Error::from_text(format!("{} does not contain a size!", lba_name)))?), is_lz4)?;
            idx += 1;
        }

        else {
            return Err(Error::from_text(format!("Could not locate file {} on disk", lba_name)));
        }
    }

    Ok(())
}

fn skip_to_lba_header(content: &mut RawData) -> &mut [u8] {
    let overlay_header_size = 0;

    &mut content[overlay_header_size..]
}

fn load_lba_names(lba_source: PathBuf) -> Result<LBANameVec, Error> {
    const LBA_DECLARATION:&'static str = "__jabyengine_request_lba_for";
    fn get_part_of_interest(file: String, lba_source: &PathBuf) -> Result<String, Error> {
        const LBA_START_POINT:&'static str = "__jabyengine_start_lba_request";
        const LBA_END_POINT:&'static str   = "__jabyengine_end_lba_request";

        let start = file.find(LBA_START_POINT).ok_or_else(|| {
            Error::from_text(format!("No LBA start found in file {}", lba_source.to_string_lossy()))
        })? + LBA_START_POINT.len();
    
        let end = file.find(LBA_END_POINT).ok_or_else(|| {
            Error::from_text(format!("No LBA end found in file {}", lba_source.to_string_lossy()))
        })?;

        Ok(file[start..end].to_owned())
    }

    let file_content = {
        match read_file_to_string(&lba_source) {
            Ok(data) => data,
            Err(_)   => return Ok(LBANameVec::new())
        }
    };
    let     file      = file_content.chars().without_comments(languages::c()).collect::<String>();
    let     file      = get_part_of_interest(file, &lba_source)?;
    let mut lba_names = Vec::new();

    for pair in file.split(LBA_DECLARATION) {
        if let Some(mut start) = pair.find('"') {
            start += 1;
            if let Some(end) = pair[start..].find('"') {
                let mut lba_name = pair[start..(start + end)].to_owned();
                if lba_name.find(';').is_none() {
                    lba_name.push_str(";1");
                }
                lba_names.push(lba_name);
            }
        }
    }

    Ok(lba_names)
}