//! Document and extract Monster World IV's data formats.
//!
//! # ROM Memory Map
//!
//! | From | To | Description |
//! |-|-|-|
//! | `0x000000` | `0x000200` | ROM header. |
//! | | | *FIXME* |
//! | `0x034b62` | `0x034cd5` | Credits-related program code. |
//! | `0x034cd6` | `0x034e8d` | Credits script. Not the same format as the dialogs scripts. |
//! | `0x034e8e` | `0x034f79` | Credits-related data. |
//! | `0x034f7a` | `0x037fff` | Filler, only `0xff`. |
//! | `0x038000` | `0x039809` | Z80 program code, copied to `[0xa00000..=0xa01809]`. |
//! | *FIXME* | *FIXME* | |
//! | `0x068000` | `0x07268b` | *FIXME* Animations, likely. |
//! | `0x07268c` | `0x07bfff` | Filler, only `0xff`. |
//! | `0x07c000` | *FIXME* | References to Japanese [scripts](script), [characters encoding](script::encoding::Encoding), [glyph graphics](glyph), and much more but I don't know what yet. |
//! | *FIXME* | *FIXME* | |
//! | `0x07de72` | `0x080ddf` | Japanese [glyphs](glyph). Maps a character to its graphics. A table followed by the graphics it points to. |
//! | `0x080de0` | `0x080f3f` | Japanese [characters encoding](script::encoding::Encoding). Maps a single-byte encoded character to its two bytes character value. |
//! | `0x080f40` | `0x087993` | Japanese [dialogs](script::id::Dialog). A table followed by the scripts it points to. |
//! | `0x087994` | `0x088234` | Japanese incluable scripts, see [`Character`](script::id::Character), [`Item`](script::id::Item), [`Location`](script::id::Location) and [`Snippet`](script::id::Snippet). A table of tables and the data they jump to. |
//! | *FIXME* | *FIXME* | |
//! | `0x0c0000` | `0x154eff` | Indexed and compressed graphics table. |
//! | `0x154f00` | `0x154f3f` | References to English [scripts](script), [characters encoding](script::encoding::Encoding), and [glyph graphics](glyph). |
//! | `0x154f40` | `0x154fff` | Program code related to [scripts](script) and [glyphs](glyph). |
//! | `0x155000` | `0x155dff` | English [glyphs](glyph). Maps a character to its graphics. A table followed by the graphics it points to. |
//! | `0x155e00` | `0x1565ff` | Program code related to save ram. |
//! | `0x156600` | `0x1567ff` | English [characters encoding](script::encoding::Encoding). Maps a single-byte encoded character to its two bytes character value. |
//! | `0x156800` | `0x157487` | English incluable scripts, see [`Character`](script::id::Character), [`Item`](script::id::Item), [`Location`](script::id::Location) and [`Snippet`](script::id::Snippet). A table of tables and the data they jump to. |
//! | `0x157488` | `0x157fff` | Filler, only `0xff`. |
//! | `0x158000` | `0x162057` | English [dialogs](script::id::Dialog). A table followed by the scripts it points to. |
//! | `0x162058` | `0x163fff` | Filler, only `0xff`. |
//! | `0x164000` | `0x164059` | Unknown program code, possibly related to the Game Over screen. |
//! | `0x16405a` | `0x1641d9` | Unknown data, possibly related to the Game Over screen. |
//! | `0x1641da` | `0x164ad9` | Graphics for the Game Over screen. |
//! | `0x164ada` | `0x1fffff` | Filler, only `0xff`. |

// FIXME Complete the memory map
// FIXME At 0x034cd6 there's the text for the Credits screen. At first glance it doesn't match
// how the rest of the script is handled.
// FIXME Around 0x0aaf62 there's code related to palette effects.

// TODO Extract the remaining compressed data.
// TODO Extract Asha's tiles.
// TODO Check is the space character in English and the special space commands rended at the same
// speed

mod condition;
mod data;
mod entity;
mod gfx;
pub mod glyph;
mod num_read;
mod palette;
pub mod script;
mod somethinghuffman;

use gfx::{Bpp, Gfx};
use num_read::NumRead;
use palette::Palette;
use std::fs::File;
use std::io::{BufReader, Read, Result as IoResult, Seek, SeekFrom};
use std::path::Path;

impl<R: Read> NumRead for BufReader<R> {}

#[derive(Clone, Copy, Debug, PartialEq)]
enum Compression {
    Unknown,
    SomethingHuffman,
}

#[derive(Clone, Copy, Debug)]
struct DataRef {
    compression: Option<Compression>,
    offset: u32,
}

impl DataRef {
    fn get_data(&self, file: &File) -> IoResult<(Vec<u8>, usize)> {
        let mut reader = BufReader::new(file);
        reader.seek(SeekFrom::Start(self.offset as u64))?;
        const DATA_SIZE: usize = 0x400;

        let (data, source_size) = match self.compression {
            None => {
                let mut data = Vec::with_capacity(DATA_SIZE);
                reader.take(DATA_SIZE as u64).read_to_end(&mut data)?;
                (data, DATA_SIZE)
            }
            Some(Compression::Unknown) => {
                // FIXME
                (vec![0; DATA_SIZE], 0)
            }
            Some(Compression::SomethingHuffman) => {
                let data = somethinghuffman::uncompress(&mut reader)?;
                (
                    data,
                    reader.stream_position()? as usize - self.offset as usize,
                )
            }
        };

        if data.len() != DATA_SIZE {
            // FIXME
            panic!();
        }

        Ok((data, source_size))
    }

    fn to_png(&self, file: &File, out_dir: &Path) -> IoResult<()> {
        let offset = self.offset;
        let name = format!("{:06x}", offset);

        let (data, _) = self.get_data(&file)?;

        let png = gfx::as_png(&data, 8, Bpp::Four(Palette::SpritesPrimary))?;

        let png_name = format!("{name}.png");
        let png_out = out_dir.join(png_name);
        std::fs::write(png_out, png)?;

        Ok(())
    }

    // fn to_tests(&self, file: &File, out_dir: &Path) -> IoResult<()> {
    //     let offset = self.offset as u64;
    //     let name = format!("{:06x}", offset);

    //     let (expected, source_size) = self.get_data(&file)?;

    //     let expected_name = format!("{name}.expected");
    //     let expected_out = out_dir.join(expected_name);
    //     std::fs::write(expected_out, expected)?;

    //     let mut reader = BufReader::new(file);
    //     reader.seek(SeekFrom::Start(self.offset as u64))?;
    //     let mut test = Vec::with_capacity(source_size);
    //     reader.take(source_size as u64).read_to_end(&mut test)?;

    //     let test_name = format!("{name}.test");
    //     let test_out = out_dir.join(test_name);
    //     std::fs::write(test_out, test)?;

    //     Ok(())
    // }
}

fn read_data_refs(file: &File, offset: u32, length: u32) -> IoResult<Vec<Option<DataRef>>> {
    let mut reader = BufReader::new(file);
    reader.seek(SeekFrom::Start(offset as u64))?;
    let mut buffer = Vec::with_capacity(length as usize);
    reader.take(length as u64).read_to_end(&mut buffer)?;

    Ok(buffer
        .chunks_exact(4)
        .map(|data_ref| match data_ref[0] {
            0 => Some(DataRef {
                compression: Some(Compression::Unknown),
                offset: u32::from_be_bytes([0, data_ref[1], data_ref[2], data_ref[3]]),
            }),
            1 => Some(DataRef {
                compression: None,
                offset: u32::from_be_bytes([0, data_ref[1], data_ref[2], data_ref[3]]),
            }),
            2 => Some(DataRef {
                compression: Some(Compression::SomethingHuffman),
                offset: u32::from_be_bytes([0, data_ref[1], data_ref[2], data_ref[3]]),
            }),
            _ => None,
        })
        .collect())
}

fn main() -> IoResult<()> {
    let rom_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("assets")
        .join("Monster World IV (USA, Europe) (En, Ja) (Virtual Console).gen");
    let rom_file = File::open(rom_path)?;

    let gfx_dir = Path::new("data/src/gfx");
    std::fs::create_dir_all(&gfx_dir)?;

    const DATA_REFS_TABLE_OFFSET: u32 = 0x0c0000;
    const DATA_REFS_TABLE_SPAN: u32 = 0x1300;
    let data_refs = read_data_refs(&rom_file, DATA_REFS_TABLE_OFFSET, DATA_REFS_TABLE_SPAN)?;
    let result: IoResult<()> = data_refs
        .iter()
        .filter_map(|data_ref| *data_ref)
        .map(|data_ref| data_ref.to_png(&rom_file, &gfx_dir))
        .collect();
    result?;

    // Exports the same graphics data as a single image. This doesn't filter out invalid data and
    // just draws 32 transparent tiles instead.
    let columns = 32;
    let png = data_refs
        .iter()
        .map(|data_ref| match data_ref {
            Some(data_ref) => data_ref.get_data(&rom_file).map(|(data, _)| {
                let mut data = gfx::Gfx::new(data, Bpp::Four(Palette::SpritesPrimary));
                data.columned(columns);
                data
            }),
            None => Ok(gfx::Gfx::new(
                vec![0u8; 1024],
                Bpp::Four(Palette::SpritesPrimary),
            )),
        })
        .collect::<IoResult<Vec<Gfx>>>()?
        .iter_mut()
        .reduce(|acc, mut e| acc.reduce(&mut e))
        .unwrap()
        .png(columns)?;

    std::fs::write(gfx_dir.join("data.png"), png)?;

    glyph::extract(&rom_file)?;
    script::extract(&rom_file)?;

    // let tests_out_dir = Path::new("src/somethinghuffman");
    // data_refs
    //     .iter()
    //     .filter_map(|data_ref| *data_ref)
    //     .filter(|data_ref| data_ref.compression == Some(Compression::SomethingHuffman))
    //     .for_each(|data_ref| data_ref.to_tests(&rom_file, &tests_out_dir).unwrap());

    // println!("{:#?}", *data::DATA_0C0000);

    Ok(())
}
