use std::collections::HashMap;

use crate::error::BioLibError;

pub struct ModuleInput;

impl ModuleInput {
    pub fn serialize(
        stdin: &[u8],
        arguments: &[String],
        files: &HashMap<String, Vec<u8>>,
    ) -> crate::Result<Vec<u8>> {
        for path in files.keys() {
            if path.contains("//") {
                return Err(BioLibError::Validation(format!(
                    "File path '{path}' contains double slashes which are not allowed"
                )));
            }
        }

        let mut data = Vec::new();

        // version (1 byte) + package_type (1 byte)
        data.push(1u8); // version
        data.push(1u8); // package_type = ModuleInput

        // stdin length (8 bytes, big-endian)
        data.extend_from_slice(&(stdin.len() as u64).to_be_bytes());

        // argument data length (4 bytes, big-endian)
        let argument_len: usize = arguments.iter().map(|arg| arg.len() + 2).sum();
        data.extend_from_slice(&(argument_len as u32).to_be_bytes());

        // file data length (8 bytes, big-endian)
        let file_data_len: usize = files
            .iter()
            .map(|(path, file_data)| file_data.len() + path.len() + 12)
            .sum();
        data.extend_from_slice(&(file_data_len as u64).to_be_bytes());

        // stdin data
        data.extend_from_slice(stdin);

        // arguments
        for argument in arguments {
            let encoded = argument.as_bytes();
            data.extend_from_slice(&(encoded.len() as u16).to_be_bytes());
            data.extend_from_slice(encoded);
        }

        // files
        for (path, file_data) in files {
            let encoded_path = path.as_bytes();
            data.extend_from_slice(&(encoded_path.len() as u32).to_be_bytes());
            data.extend_from_slice(&(file_data.len() as u64).to_be_bytes());
            data.extend_from_slice(encoded_path);
            data.extend_from_slice(file_data);
        }

        Ok(data)
    }

    pub fn deserialize(bbf: &[u8]) -> crate::Result<ModuleInputData> {
        let mut pointer = 0usize;

        let version = bbf[pointer];
        pointer += 1;
        let package_type = bbf[pointer];
        pointer += 1;

        if version != 1 {
            return Err(BioLibError::BinaryFormat(format!(
                "Unsupported version: {version}"
            )));
        }
        if package_type != 1 {
            return Err(BioLibError::BinaryFormat(format!(
                "Unsupported package type: {package_type}"
            )));
        }

        let stdin_len = read_u64_be(bbf, &mut pointer) as usize;
        let argument_data_len = read_u32_be(bbf, &mut pointer) as usize;
        let files_data_len = read_u64_be(bbf, &mut pointer) as usize;

        let stdin = bbf[pointer..pointer + stdin_len].to_vec();
        pointer += stdin_len;

        let end_of_arguments = pointer + argument_data_len;
        let mut arguments = Vec::new();
        while pointer < end_of_arguments {
            let arg_len = read_u16_be(bbf, &mut pointer) as usize;
            let arg = String::from_utf8_lossy(&bbf[pointer..pointer + arg_len]).to_string();
            pointer += arg_len;
            arguments.push(arg);
        }

        let end_of_files = pointer + files_data_len;
        let mut files = HashMap::new();
        while pointer < end_of_files {
            let path_len = read_u32_be(bbf, &mut pointer) as usize;
            let data_len = read_u64_be(bbf, &mut pointer) as usize;
            let path = String::from_utf8_lossy(&bbf[pointer..pointer + path_len]).to_string();
            pointer += path_len;
            let file_data = bbf[pointer..pointer + data_len].to_vec();
            pointer += data_len;
            files.insert(path, file_data);
        }

        Ok(ModuleInputData {
            stdin,
            arguments,
            files,
        })
    }
}

#[derive(Debug)]
pub struct ModuleInputData {
    pub stdin: Vec<u8>,
    pub arguments: Vec<String>,
    pub files: HashMap<String, Vec<u8>>,
}

fn read_u64_be(data: &[u8], pointer: &mut usize) -> u64 {
    let value = u64::from_be_bytes(data[*pointer..*pointer + 8].try_into().unwrap());
    *pointer += 8;
    value
}

fn read_u32_be(data: &[u8], pointer: &mut usize) -> u32 {
    let value = u32::from_be_bytes(data[*pointer..*pointer + 4].try_into().unwrap());
    *pointer += 4;
    value
}

fn read_u16_be(data: &[u8], pointer: &mut usize) -> u16 {
    let value = u16::from_be_bytes(data[*pointer..*pointer + 2].try_into().unwrap());
    *pointer += 2;
    value
}
