
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use binrw::{binrw, BinRead, binwrite, NullString, BinReaderExt, BinWriterExt, BinWrite};
use std::fmt;
use crate::prelude::Instruction;

#[binrw]
#[brw(magic = b"DSAX", little)]
#[derive(Debug, Clone)]
pub struct DseHeader {
    version: u16,
    flags: u16,
    pub entry_point: u32,

    symbol_table_offset: u32,
    symbol_table_size: u32,

    string_table_offset: u32,
    string_table_size: u32,

    data_offset: u32,
    data_size: u32,

    pub text_offset: u32,
    text_size: u32
}

#[binrw]
#[brw(little)]
#[derive(Debug, Clone)]
pub struct SymbolEntry {
    pub name_offset: u32,
    pub value: u32,
    pub section: u8,
    pub binding: u8,
    pub _pad: u16
}

#[derive(Debug, Clone)]
pub struct DseExecutable {
    pub header: DseHeader,
    pub symbols: Vec<SymbolEntry>,
    pub string_table: Vec<u8>,
    pub data: Vec<u8>,
    pub text: Vec<u32>
}

impl DseExecutable {
    pub fn load(path: PathBuf) -> binrw::BinResult<Self> {
        let mut file = File::open(path).map_err(|e| binrw::Error::Io(e))?;
        Self::read(&mut file)
    }

    pub fn read<R: Read + Seek>(reader: &mut R) -> binrw::BinResult<Self> {
        let header = DseHeader::read(reader)?;

        reader.seek(SeekFrom::Start(header.symbol_table_offset as u64))?;
        let symbols: Vec<SymbolEntry> = (0..header.symbol_table_size)
            .map(|_| SymbolEntry::read(reader))
            .collect::<binrw::BinResult<_>>()?;

        reader.seek(SeekFrom::Start(header.string_table_offset as u64))?;
        let mut string_table = vec![0u8; header.string_table_size as usize];
        reader.read_exact(&mut string_table).map_err(|e| binrw::Error::Io(e))?;

        reader.seek(SeekFrom::Start(header.data_offset as u64))?;
        let mut data = vec![0u8; header.data_size as usize];
        reader.read_exact(&mut data).map_err(|e| binrw::Error::Io(e))?;

        reader.seek(SeekFrom::Start(header.text_offset as u64))?;
        let text: Vec<u32> = (0..header.text_size)
            .map(|_| reader.read_le::<u32>())
            .collect::<binrw::BinResult<_>>()?;

        Ok(DseExecutable { header, symbols, string_table, data, text })
    }

    pub fn save(&self, path: PathBuf) -> binrw::BinResult<()> {
        let mut file = File::create(path)
            .map_err(|e| binrw::Error::Io(e))?;
        self.write(&mut file)
    }

    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> binrw::BinResult<()> {
        self.header.write(writer)?;

        for symbol in &self.symbols {
            symbol.write(writer)?;
        }

        writer.write_all(&self.string_table)?;
        writer.write_all(&self.data)?;

        for word in &self.text {
            writer.write_le(word)?;
        }

        Ok(())
    }

    pub fn new(
        symbols: &[SymbolEntry],
        string_table: &[u8],
        data: &[u8],
        text: &[u32],
        entry_point: u32,
    ) -> binrw::BinResult<DseExecutable> {
        // Derive sizes from real serialization instead of hand-counted constants.
        let header_size = {
            let mut buf = Cursor::new(Vec::new());
            // dummy header just to measure length; fields' values don't matter here
            let dummy = DseHeader {
                version: 0, flags: 0, entry_point: 0,
                symbol_table_offset: 0, symbol_table_size: 0,
                string_table_offset: 0, string_table_size: 0,
                data_offset: 0, data_size: 0,
                text_offset: 0, text_size: 0,
            };
            dummy.write(&mut buf)?;
            buf.into_inner().len() as u32
        };

        let symbol_entry_size = if symbols.is_empty() {
            // measure using a zeroed entry even if symbols is empty
            let mut buf = Cursor::new(Vec::new());
            SymbolEntry { name_offset: 0, value: 0, section: 0, binding: 0, _pad: 0 }
                .write(&mut buf)?;
            buf.into_inner().len() as u32
        } else {
            let mut buf = Cursor::new(Vec::new());
            symbols[0].write(&mut buf)?;
            buf.into_inner().len() as u32
        };

        let symbol_table_offset = header_size;
        let symbol_table_bytes = symbols.len() as u32 * symbol_entry_size;
        let string_table_offset = symbol_table_offset + symbol_table_bytes;
        let data_offset = string_table_offset + string_table.len() as u32;
        let text_offset = data_offset + data.len() as u32;

        let header = DseHeader {
            version: 1,
            flags: 0,
            entry_point,
            symbol_table_offset,
            symbol_table_size: symbols.len() as u32,
            string_table_offset,
            string_table_size: string_table.len() as u32,
            data_offset,
            data_size: data.len() as u32,
            text_offset,
            text_size: text.len() as u32,
        };

        Ok(DseExecutable {
            header,
            symbols: symbols.to_vec(),
            string_table: string_table.to_vec(),
            data: data.to_vec(),
            text: text.to_vec(),
        })
    }

    /// Produces the runtime memory image: TEXT followed immediately by DATA,
    /// matching the exact layout `resolve_address` assumed when the assembler
    /// resolved symbol addresses. This is NOT the same as the on-disk file
    /// layout (header/symtab/strtab/data/text) — this is what actually needs
    /// to end up in emulator memory.
    pub fn to_memory_image(&self) -> Vec<u8> {
        let mut image = Vec::with_capacity(self.text.len() * 4 + self.data.len());

        for word in &self.text {
            image.extend_from_slice(&word.to_le_bytes());
        }
        image.extend_from_slice(&self.data);

        image
    }

    /// Runtime address where DATA begins, given TEXT is loaded at `text_base`.
    /// Matches the assembler's `resolve_address` convention exactly.
    pub fn data_base(&self, text_base: u32) -> u32 {
        text_base + (self.text.len() as u32 * 4)
    }

    /// Absolute runtime entry point address, given TEXT is loaded at `text_base`.
    /// `self.header.entry_point` is already an absolute address under the
    /// assumption text_base == 0, so this just re-bases it if you're loading
    /// somewhere other than address 0.
    pub fn entry_point(&self, text_base: u32) -> u32 {
        text_base + self.header.entry_point
    }
}

// Display implementations for dumping binary contents
impl fmt::Display for DseHeader {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "DseHeader {{ version: {}, flags: {}, entry_point: 0x{:08X}, symbol_table_offset: 0x{:08X}, symbol_table_size: {}, string_table_offset: 0x{:08X}, string_table_size: {}, data_offset: 0x{:08X}, data_size: {}, text_offset: 0x{:08X}, text_size: {} }}",
               self.version,
               self.flags,
               self.entry_point,
               self.symbol_table_offset,
               self.symbol_table_size,
               self.string_table_offset,
               self.string_table_size,
               self.data_offset,
               self.data_size,
               self.text_offset,
               self.text_size)
    }
}

impl fmt::Display for SymbolEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SymbolEntry {{ name_offset: 0x{:08X}, value: 0x{:08X}, section: {}, binding: {} }}",
               self.name_offset,
               self.value,
               self.section,
               self.binding)
    }
}

impl fmt::Display for DseExecutable {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Header
        writeln!(f, "{}", self.header)?;
        // Symbols
        writeln!(f, "Symbols:")?;
        for s in &self.symbols {
            writeln!(f, "  {}", s)?;
        }
        // String table as hex dump
        writeln!(f, "String Table ({} bytes):", self.string_table.len())?;
        for chunk in self.string_table.chunks(16) {
            write!(f, "{}", String::from_utf8_lossy(chunk))?;
            // for byte in chunk { write!(f, "{:02X} ", byte)?; }
            writeln!(f)?;
        }
        // Data section
        writeln!(f, "Data ({} bytes):", self.data.len())?;
        for chunk in self.data.chunks(16) {
            write!(f, "  ")?;
            for byte in chunk { write!(f, "{:02X} ", byte)?; }
            for byte in chunk { write!(f, "{} ", *byte as char)?; }
            writeln!(f)?;
        }
        // Text section as Instructions
        writeln!(f, "Text ({} words):", self.text.len())?;
        for word in &self.text {
            writeln!(f, "  {:#010X} {:?}", word, Instruction(*word))?;
        }
        Ok(())
    }
}

#[test]
fn symbol_entry_size_is_12_bytes() {
    let mut buf = Cursor::new(Vec::new());
    SymbolEntry { name_offset: 0, value: 0, section: 0, binding: 0, _pad: 0 }
        .write(&mut buf).unwrap();
    assert_eq!(buf.into_inner().len(), 12);
}