use anyhow::{bail, Result};
use indexmap::IndexSet;
use itertools::Itertools;
use std::collections::HashMap;

use crate::parser;

pub mod instruction;
pub mod templates;

pub use instruction::Instruction;

pub trait Compilable<T, U> {
    fn compile(&self, data: &T) -> Result<U>;
}

#[derive(Debug)]
pub struct Word {
    pub id: usize,
    pub instructions: Vec<Instruction>,
    pub times_used: usize,
}

#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Condition {
    pub if_instructions: Vec<Instruction>,
    pub else_instructions: Vec<Instruction>,
}

impl Condition {
    pub fn only_if(&self) -> bool {
        self.else_instructions.is_empty()
    }
}

#[derive(Debug)]
pub enum CallableId {
    Word(String),
    Condition(usize),
}

#[derive(Debug)]
pub struct Compiler {
    pub strings: IndexSet<String>,
    pub words: HashMap<String, Word>,
    pub conditions: Vec<Condition>,
}

impl Compiler {
    pub fn default() -> Self {
        Self {
            strings: IndexSet::new(),
            words: HashMap::new(),
            conditions: vec![],
        }
    }

    pub fn generate_instructions(
        &mut self,
        body: parser::ast::Body,
        optimize: bool,
    ) -> Result<Vec<Instruction>> {
        let mut instructions: Vec<Instruction> = vec![];
        let mut iter = body.into_iter().peekable();
        while let Some(node) = iter.next() {
            match node {
                _ if optimize && iter.next_if_eq(&node).is_some() => {
                    let count = iter.by_ref().peeking_take_while(|n| *n == node).count() + 2;
                    instructions.push(Instruction::Multiple {
                        instruction: Box::new(
                            self.generate_instructions(vec![node], optimize)?
                                .into_iter()
                                .next()
                                .unwrap(),
                        ),
                        count,
                    });
                }
                parser::ast::Node::Comment(_) => {}
                parser::ast::Node::String { mode, string } => {
                    instructions.push(match mode.as_str() {
                        "." => {
                            let id = self.strings.insert_full(string).0;
                            Instruction::StringPrint(id)
                        }
                        "r" => {
                            let id = self.strings.insert_full(string).0;
                            Instruction::StringReference(id)
                        }
                        "asm" => Instruction::AsmQuote(string),
                        _ => bail!("Unknown string mode: {}", mode),
                    });
                }
                parser::ast::Node::Number(x) => {
                    instructions.push(instruction::Instruction::Push(x));
                }
                parser::ast::Node::WordDefinition {
                    name,
                    stack: _,
                    body,
                } => {
                    if Instruction::from_word(&name).is_some() {
                        bail!("Word already exists as compiler instruction: {}", name);
                    } else if self.words.contains_key(&name) {
                        bail!("Word already exists as user word definition: {}", name);
                    }

                    let instructions = self.generate_instructions(body, optimize)?;
                    self.words.insert(
                        name.to_string(),
                        Word {
                            id: self.words.len(),
                            instructions,
                            times_used: 0,
                        },
                    );
                }
                parser::ast::Node::Condition { if_body, else_body } => {
                    let if_instructions = self.generate_instructions(if_body, optimize)?;
                    let else_instructions = self.generate_instructions(else_body, optimize)?;
                    let id = self.conditions.len();
                    self.conditions.push(Condition {
                        if_instructions: if_instructions.clone(),
                        else_instructions: else_instructions.clone(),
                    });
                    instructions.push(Instruction::Condition(id));
                }
                parser::ast::Node::Word(x) => {
                    if let Some(ins) = Instruction::from_word(&x) {
                        instructions.push(ins);
                    } else if let Some(w) = self.words.get_mut(&x) {
                        w.times_used += 1;
                        instructions.push(Instruction::Call(x));
                    } else {
                        bail!("Word does not exist: {}", x);
                    }
                }
            }
        }

        Ok(instructions)
    }

    pub fn embed(&self, body: hence::parser::ast::Body) -> Result<String> {
        let strings = self
            .strings
            .iter()
            .enumerate()
            .map(|(id, s)| templates::IdLike {
                id,
                data: s.to_owned(),
            })
            .collect();

        let conditions = self
            .conditions
            .iter()
            .enumerate()
            .map(|(id, c)| {
                Ok(templates::IdLike {
                    id,
                    data: (
                        c.if_instructions
                            .iter()
                            .map(|ins| ins.compile(self))
                            .collect::<Result<Vec<_>>>()?
                            .into_iter()
                            .flatten()
                            .collect::<Vec<_>>(),
                        c.else_instructions
                            .iter()
                            .map(|ins| ins.compile(self))
                            .collect::<Result<Vec<_>>>()?
                            .into_iter()
                            .flatten()
                            .collect::<Vec<_>>(),
                    ),
                })
            })
            .collect::<Result<Vec<_>>>()?;

        let words = self
            .words
            .iter()
            .filter(|(_, w)| w.times_used > 1)
            .sorted_by(|a, b| Ord::cmp(&a.1.id, &b.1.id))
            .map(|(name, w)| {
                Ok(templates::IdLike {
                    id: w.id,
                    data: (
                        name.to_owned(),
                        w.instructions
                            .iter()
                            .map(|ins| ins.compile(self))
                            .collect::<Result<Vec<_>>>()?
                            .into_iter()
                            .flatten()
                            .collect(),
                    ),
                })
            })
            .collect::<Result<Vec<_>>>()?;

        Ok(templates::DefaultTemplate {
            strings,
            conditions,
            words,
            main: body,
        }
        .to_string())
    }
}

pub fn compile(ast: parser::ast::AST, optimize: bool) -> Result<String> {
    let mut compiler = Compiler::default();
    let instructions = compiler.generate_instructions(ast.body, optimize)?;

    Ok(compiler.embed(
        instructions
            .iter()
            .map(|ins| ins.compile(&compiler))
            .collect::<Result<Vec<hence::parser::ast::Body>>>()?
            .into_iter()
            .flatten()
            .collect(),
    )?)
}
