use std::fmt;
use toml::Value;
use std::fs::File;
use std::io::Write;
use clap::{command, Arg};

static TEMPLATE : &str = include_str!("../templates/errors.hpp.tmpl");

enum Error{
    TomlParsing(String),
    WriteToGeneratedFile(String)
}

impl fmt::Display for Error{
    fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
        match self{
            Error::TomlParsing(e) => write!(f, "Error in pasing toml: {}", e),
            Error::WriteToGeneratedFile(e) => write!(f, "Error in trying to write to the generated file: {}", e),
        }
    }
}


struct CppError{
   name : String,
   message : String,
}

struct Category{
    prefix : String,
    error_list : Vec<CppError>,
}

struct Details{
    namespace : String,
}

struct ParserResult{
    det : Details,
    categories : Vec<Category>,
}

struct Parser{
    file_path : String,
    result : ParserResult,
}

impl Details{
    fn new() -> Self{
        Self{namespace : String::from("mylib")}
    }
}
impl ParserResult{
    fn new() -> Self{
        Self{ det : Details::new(), categories : Vec::new()}
    }
}

impl Parser{
    pub fn print(&self){
        for cat in self.result.categories.iter(){
            println!("Prefix: {}", cat.prefix); 
            for err in cat.error_list.iter(){
                println!("\t\tName: {}", err.name);
                println!("\t\tMessage: {}", err.message);
            }
        }
    }
    pub fn new(path : String) -> Self{
        Self{file_path : path, 
            result : ParserResult::new(),
        } 
    }
    pub fn go(&mut self) -> Result<(), Error> {
        let contents = std::fs::read_to_string(self.file_path.as_str()).unwrap();
        let value : Value  = contents.parse().unwrap();

        if let Value::Table(table) = value {
            for (name, section) in table.iter() {
                if name == "details"{
                    let det : Details = self.parse_details(&section)?;
                    self.result.det = det;
                    continue;
                }
                let category = self.parse_category(&section);
                match category{ 
                    Ok(cat) => {
                        self.result.categories.push(cat);
                    }
                    Err(err) => return Err(err),
                }
            }
        }
        Ok(())
    }
    fn parse_details(&self, section : &toml::Value) ->Result<Details, Error>{
        match section{
            toml::Value::Table(map) => {
                let mut det = Details::new();
                let namespace = map.get("namespace").unwrap().to_string().replace('"', "");
                det.namespace = namespace;
                Ok(det)
            }
            _ => {
                Err(Error::TomlParsing("Section is not a table".to_string()))
            }
        }
    }
    fn parse_category(&self, section : & toml::Value ) -> Result<Category, Error>{
        match section{
            toml::Value::Table(map) => {
                let prefix = map.get("prefix").unwrap().to_string().replace('"', "");
                let error_section = map.get("errors")
                    .and_then(|v| v.as_array())
                    .unwrap();

                let list : Vec<CppError> = self.parse_error_list(&error_section) ?;

                Ok(Category{prefix : prefix, error_list :list})
            }
            _ => {
                Err(Error::TomlParsing("Section is not a table".to_string()))
            }
        }
    }
    fn parse_error_list(&self, section : &Vec<toml::Value>) -> Result<Vec<CppError>,Error>{
        let mut list : Vec<CppError> = Vec::new();
        for subsection in section.iter() {
            match subsection {
                toml::Value::Table(map) => { 
                    let err_name = map.get("name")
                        .unwrap()
                        .to_string().replace('"', "");
                    let err_msg = map.get("message")
                        .unwrap()
                        .to_string().replace('"', "");
                     
                    list.push(CppError { name : err_name, message : err_msg});
                }
                _ => {
                    return Err(Error::TomlParsing("Section is not a table".to_string()));
                }
            }
        }

        Ok(list)
    }    
}

struct Generator{
    info : ParserResult,
    output_dir : String
}

impl Generator {
    fn new(list : ParserResult, output_dir : String) -> Self{
        Self { info : list, output_dir : output_dir}
    }
    fn go(&self) -> Result<(), Error>{
        let mut category_id = 0;
        // creating the error code list string
        let mut error_code_list : String = String::new();
        for category in self.info.categories.iter() {
            let mut error_id = 0;
            for err in category.error_list.iter() {
                error_code_list.push_str(Self::create_error_code(&category.prefix, category_id, &err.name, error_id).as_str());
                error_id += 1;
            }
            category_id += 1;
        }
        // creating the error code matrix 
        let (mut error_matrix, max) = self.error_matrix_decl();
        error_matrix.push_str(self.error_matrix_content(max).as_str());
       
        // probably some capacity reserve optimisation could be done here
        let template_content = TEMPLATE
            .replace("{{ NAMESPACE }}", self.info.det.namespace.as_str()) 
            .replace("{{ ERROR_MSG_MATRIX }}", error_matrix.as_str())
            .replace("{{ ERROR_LIST }}", error_code_list.as_str());
       
        let output_path = format!("{}errors.gen.hpp", self. output_dir);
        let mut f = File::create(output_path).unwrap();
        let err = f.write_all(template_content.as_bytes());
        if let Err(err) = err {
            Err(Error::WriteToGeneratedFile(err.to_string()))
        }else{
            Ok(())
        }

    }
    fn error_matrix_decl(&self) -> (String, usize){
        let mut max = 0;
        for category in self.info.categories.iter(){
            if category.error_list.len() > max{
                max = category.error_list.len();
            }
        }
        return (format!("\tconstexpr std::string_view matrix[][{}] = {{\n", max), max)
    }
    fn error_matrix_row(category : &Category, max : usize) -> String{
        let mut row_str = String::from("\t{");
        for err in category.error_list.iter(){
            // we format an element
            let mut elem_str = err.message.clone();
            elem_str.insert(0, '"');
            elem_str.push_str("\",");
            row_str.push_str(elem_str.as_str());
        }
        for _ in category.error_list.len()..max {
            let elem_str = "\"\","; 
            row_str.push_str(elem_str);
        }
        row_str.pop();
        row_str.push_str("},\n");

        return row_str;
    }
    fn error_matrix_content(&self, max : usize) -> String{
        let mut result = String::new(); 
        for category in self.info.categories.iter(){
            result.push_str(Self::error_matrix_row(&category,max).as_str());
        }
        // we pop the last ',\n'
        result.pop();
        result.pop();
        result.push_str("};");
        return result;
    }
    fn create_error_code(category_name : &String, category_id : i32, err_name : &String, err_id : i32) -> String{
        format!("\tconstexpr code {}_{} = {{ {}, {}, format }};\n", 
                category_name, 
                err_name, 
                category_id, 
                err_id)
    }
}



fn run_cli() -> clap::ArgMatches{
    command!().arg(
        Arg::new("input")
        .short('i')
        .long("input")
        .required(true)
    ) 
    .arg(
        Arg::new("out")
        .short('o')
        .long("out")
        .required(false)
    ).get_matches()
}

fn main() {
    let args = run_cli();
    let input = args.get_one::<String>("input").unwrap(); 
    let out = match args.get_one::<String>("out"){
        Some( val ) => {
            let mut clone = val.clone();
            if val.ends_with('/') == false {
                clone.push('/');
            }
            clone
        }
        None => String::from("")
    };

    let mut p = Parser::new(input.clone());

    let mut err = p.go();
    if let Err(err) = err {
        println!("Parsing error: {}", err);
    }
    p.print();

    let g = Generator::new(p.result, out); 
    err = g.go();
    if let Err(err) = err {
        println!("Parsing error: {}", err);
    }
}
