//! Narrative text scripts.

pub mod encoding;
mod formatter;
pub mod id;
mod label;
mod lexeme;
mod position;
mod read;
mod style;
mod token;

use crate::num_read::NumRead;
use encoding::Encoding;
use formatter::Formatter as ScriptFmt;
use id::{Character, Dialog, Item, Location, Snippet};
use lexeme::Lexeme;
use position::Position;
use read::ScriptRead;
use rust_format::{Formatter as RustFormatter, RustFmt};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Error as IoError, Result as IoResult, Seek, SeekFrom};
use std::path::Path;
use token::Token;

fn read_scripts<T: strum::IntoEnumIterator, const N: usize>(
    file: &File,
    table_position: u64,
    encoding: Encoding,
) -> IoResult<String> {
    assert!(T::iter().count() == N);

    let mut reader = BufReader::new(file);

    reader.seek(SeekFrom::Start(table_position))?;
    let dialog_positions = reader
        .read_be_u16_array::<N>()?
        .iter()
        .map(|offset| Position::new(table_position as u32 + *offset as u32))
        .collect::<Vec<Position>>();

    // Reads scripts. For each of them, extract the positions jumped from it, and if we didn't read
    // them yet, queue them to be read.
    let mut lexemes = HashMap::new();

    let mut positions_to_parse = dialog_positions.clone();
    let mut positions_jumped_to = dialog_positions.clone();
    while let Some(position) = positions_to_parse.pop() {
        reader.seek(SeekFrom::Start(position.position() as u64))?;
        let new_lexemes = reader.read_script(encoding)?;
        let mut new_positions: Vec<Position> = new_lexemes
            .iter()
            .filter_map(|lexeme| lexeme.token().position())
            .collect();

        for lexeme in new_lexemes {
            lexemes.insert(lexeme.position(), lexeme);
        }

        let mut new_positions_to_parse: Vec<Position> = new_positions
            .iter()
            .filter(|position| !lexemes.contains_key(*position))
            .map(|position| *position)
            .collect();

        positions_to_parse.append(&mut new_positions_to_parse);
        positions_jumped_to.append(&mut new_positions);
    }

    let mut lexemes = lexemes.into_values().collect::<Vec<_>>();

    positions_jumped_to.sort();
    positions_jumped_to.dedup();

    // Add label lexemes before the position they are refering to for positions that are jumped to.
    lexemes.append(
        &mut positions_jumped_to
            .iter()
            .map(|position| Ok(Lexeme::new(Token::Label(*position), *position, 0)))
            .collect::<IoResult<Vec<Lexeme>>>()?,
    );

    // Sort lexemes by position, puting label lexemes first when they exist.
    lexemes.sort_by(|a, b| match a.position().cmp(&b.position()) {
        Ordering::Equal => match (a.token(), b.token()) {
            (Token::Label(_), Token::Label(_)) => Ordering::Equal,
            (Token::Label(_), _) => Ordering::Less,
            (_, Token::Label(_)) => Ordering::Greater,
            (_, _) => Ordering::Equal,
        },
        ordering => ordering,
    });
    lexemes.dedup();

    // Export the script as Rust code.
    RustFmt::default()
        .format_str(format!(
            "\
#![allow(dead_code)]
#![allow(long_running_const_eval)]
#![allow(unused_imports)]

use crate::character::Character::{{self, *}};
use crate::condition::Condition::{{self, *}};
use crate::entity::Entity;
use crate::item::Item::{{self, *}};
use crate::location::Location::{{self, *}};
use crate::save_point::SavePoint::{{self, *}};
use crate::script::Token::{{self, *}};
use crate::script::script;
use crate::snippet::Snippet::{{self, *}};
use crate::sound::Sound::{{self, *}};
use crate::style::Style::{{self, *}};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Label {{{}}}

use self::Label::*;

const fn find_label(script: &'static [Token<Label>], label: Label) -> usize {{
    let mut i = 0;
    while i < script.len() {{
        match script[i] {{
            Token::Label(l) if l as u8 == label as u8 => return i,
            _ => i += 1,
        }};
    }}
    unreachable!();
}}

pub fn dialog_for_label(index: usize) -> &'static [Token<'static, Label>] {{
    &SCRIPTS[INDEXED[index]..]
}}

pub fn dialog(index: usize) -> &'static [Token<'static, Label>] {{
    &SCRIPTS[INDEXED[index]..]
}}

const SCRIPTS: &[Token<Label>] = {};

pub const INDEXED: &[usize] = &[{}];",
            positions_jumped_to
                .iter()
                .map(|label| format!("{label:?},"))
                .collect::<String>(),
            ScriptFmt::new(lexemes).to_string(),
            dialog_positions
                .iter()
                .map(|position| format!("find_label(SCRIPTS, {:?}),\n", position))
                .collect::<String>()
        ))
        .map_err(|e| IoError::other(e))
}

/// Extracts the scripts.
///
/// This extracts the dialog scripts, as well as the incluable character names, item names, location
/// names, and snippet scripts. They are extracted as Rust files containing a continuous stream of
/// tokens containing all the scripts, and tables allowing to reach specific scripts in the stream
/// either from the cript type's index or a label.
///
/// The scripts are extracted into `data/src/script/`.
pub fn extract(file: &File) -> IoResult<()> {
    let script_dir = Path::new("data/src/script");
    std::fs::create_dir_all(&script_dir)?;

    {
        const EN_NAMES_TABLE_OFFSET: u64 = 0x156800;
        const JA_NAMES_TABLE_OFFSET: u64 = 0x087994;
        const EN_NAMES_TABLE_COUNT: usize = 4;

        let mut reader = BufReader::new(file);

        reader.seek(SeekFrom::Start(EN_NAMES_TABLE_OFFSET))?;
        let en_name_table_offsets = reader.read_be_u16_array::<EN_NAMES_TABLE_COUNT>()?;

        reader.seek(SeekFrom::Start(JA_NAMES_TABLE_OFFSET))?;
        let ja_name_table_offsets = reader.read_be_u16_array::<EN_NAMES_TABLE_COUNT>()?;

        // Incluable English character names
        // FIXME The table has 7 items but only the first 2 are valid. We need to skip the last 5.
        const EN_CHARACTERS_COUNT: usize = 2;
        let table_offset = EN_NAMES_TABLE_OFFSET + en_name_table_offsets[0] as u64;
        std::fs::write(
            script_dir.join("character_en.rs"),
            read_scripts::<Character, EN_CHARACTERS_COUNT>(file, table_offset, Encoding::English)?,
        )?;

        // Incluable Japanese character names
        // FIXME The table has 7 items but only the first 2 are valid. We need to skip the last 5.
        // const JA_CHARACTERS_COUNT: usize = 2;
        const JA_CHARACTERS_COUNT: usize = 2;
        let table_offset = JA_NAMES_TABLE_OFFSET + ja_name_table_offsets[0] as u64;
        std::fs::write(
            script_dir.join("character_ja.rs"),
            read_scripts::<Character, JA_CHARACTERS_COUNT>(file, table_offset, Encoding::Japanese)?,
        )?;

        // Incluable English item names
        // There are 65 item names followed by their 65 item descriptions.
        const EN_ITEMS_COUNT: usize = 130;
        let table_offset = EN_NAMES_TABLE_OFFSET + en_name_table_offsets[1] as u64;
        std::fs::write(
            script_dir.join("item_en.rs"),
            read_scripts::<Item, EN_ITEMS_COUNT>(file, table_offset, Encoding::English)?,
        )?;

        // Incluable Japanese item names
        // There are 65 item names followed by their 65 item descriptions.
        const JA_ITEMS_COUNT: usize = 130;
        let table_offset = JA_NAMES_TABLE_OFFSET + ja_name_table_offsets[1] as u64;
        std::fs::write(
            script_dir.join("item_ja.rs"),
            read_scripts::<Item, JA_ITEMS_COUNT>(file, table_offset, Encoding::Japanese)?,
        )?;

        // Incluable English location names
        const EN_LOCATIONS_COUNT: usize = 8;
        let table_offset = EN_NAMES_TABLE_OFFSET + en_name_table_offsets[2] as u64;
        std::fs::write(
            script_dir.join("location_en.rs"),
            read_scripts::<Location, EN_LOCATIONS_COUNT>(file, table_offset, Encoding::English)?,
        )?;

        // Incluable Japanese location names
        const JA_LOCATIONS_COUNT: usize = 8;
        let table_offset = JA_NAMES_TABLE_OFFSET + ja_name_table_offsets[2] as u64;
        std::fs::write(
            script_dir.join("location_ja.rs"),
            read_scripts::<Location, JA_LOCATIONS_COUNT>(file, table_offset, Encoding::Japanese)?,
        )?;

        // Incluable English snippets
        const EN_SNIPPETS_COUNT: usize = 31;
        let table_offset = EN_NAMES_TABLE_OFFSET + en_name_table_offsets[3] as u64;
        std::fs::write(
            script_dir.join("snippet_en.rs"),
            read_scripts::<Snippet, EN_SNIPPETS_COUNT>(file, table_offset, Encoding::English)?,
        )?;

        // Incluable Japanese snippets
        const JA_SNIPPETS_COUNT: usize = 31;
        let table_offset = JA_NAMES_TABLE_OFFSET + ja_name_table_offsets[3] as u64;
        std::fs::write(
            script_dir.join("snippet_ja.rs"),
            read_scripts::<Snippet, JA_SNIPPETS_COUNT>(file, table_offset, Encoding::Japanese)?,
        )?;

        // English dialogs
        const EN_TEXTS_TABLE_OFFSET: u64 = 0x158000;
        const EN_TEXTS_COUNT: usize = 269;
        std::fs::write(
            script_dir.join("dialog_en.rs"),
            read_scripts::<Dialog, EN_TEXTS_COUNT>(file, EN_TEXTS_TABLE_OFFSET, Encoding::English)?,
        )?;

        // Japanese dialogs
        const JA_TEXTS_TABLE_OFFSET: u64 = 0x080f40;
        const JA_TEXTS_COUNT: usize = 269;
        std::fs::write(
            script_dir.join("dialog_ja.rs"),
            read_scripts::<Dialog, JA_TEXTS_COUNT>(
                file,
                JA_TEXTS_TABLE_OFFSET,
                Encoding::Japanese,
            )?,
        )?;
    }

    Ok(())
}
