use std::path::{PathBuf,Path};
use std::collections::HashMap;
use core::error::Error;
use std::time::SystemTime;

mod lexer;
mod parser;
mod position;
mod error;
mod context;

pub use error::{CrimError,CrimResult};
pub use position::*;
pub use context::{Context,Value,MappedStructure,StatefulContext};

#[derive(Debug)]
pub struct Crimtag {
    views: HashMap<String, View>,
    sources: Vec<ViewSource>,
}

#[derive(PartialEq,Eq,Debug,Clone)]
pub enum ViewSource {
    File {
        path: PathBuf,
        loaded: SystemTime,
    },
    Static,
    External {
        key: String,
    },
}

#[derive(Debug)]
struct View {
    name: String,
    source: usize,
    outputs: Vec<Output>,
//    theme: Option<String>
    layout: Option<String>,
}

impl View {
    fn process<T: MappedStructure>(&self, input: &T) -> CrimResult<Context> {
        let mut out_vars = Context::new();
        for output in &self.outputs {
            let mut buf = String::new();
            let sc = StatefulContext::root( input );
            exec( &sc, &output.code, &mut buf )?;
            out_vars.insert( output.name.clone(), Value::String(buf) );
        }
        Ok(out_vars)
    }
}

#[derive(Debug)]
struct Output {
    name: String,
    code: Vec<Token>,
}

type Properties = HashMap<String, String>;

#[derive(Debug)]
enum Token {
    Loop(Identifier, Vec<Token>),
    Show(Identifier,Properties),
    If(Identifier, Vec<Token>, Vec<Token>),
    Text(String),
}

#[derive(Debug,Clone,PartialEq)]
pub enum IdentifierValue {
    Root,
    Name(String),
    Index(usize),
}

pub type Identifier = Vec<IdentifierValue>;

impl Crimtag {
    pub fn new() -> Self {
        Self {
            views: HashMap::new(),
            sources: vec![ViewSource::Static],
        }
    }

    fn id_source(&mut self, src: &ViewSource ) -> usize {
        for idx in 0..self.sources.len() {
            if self.sources[idx] == *src {
                return idx;
            }
        }

        // Nothing found, add a new one
        let idx = self.sources.len();
        self.sources.push( src.clone() );
        idx
    }

    pub fn load_file(&mut self, path: &Path) -> Result<(),Box::<dyn Error>> {
        let time = if let Ok(meta) = path.metadata() {
            if let Ok(t) = meta.modified() {
                t
            } else if let Ok(t) = meta.created() {
                t
            } else {
                SystemTime::now()
            }
        } else {
            SystemTime::now()
        };
        let source_id = self.id_source(
            &ViewSource::File {
                path: path.to_path_buf(),
                loaded: time,
                }
            );
        let p = parser::Parser::new();
        self.register_views(
            p.parse(
                &String::from_utf8( std::fs::read( path )? )?,
                source_id
                )?,
            )?;
        Ok(())
    }

    pub fn load_static(&mut self, data: &str) -> CrimResult<()> {
        let source_id = self.id_source( &ViewSource::Static );
        let p = parser::Parser::new();
        self.register_views( p.parse( &data, source_id )? )
    }

    pub fn load_external(&mut self, key: &str, data: &str) -> CrimResult<()> {
        let source_id = self.id_source(
            &ViewSource::External{ key: key.to_string()}
            );
        
        let p = parser::Parser::new();    
        self.register_views(
            p.parse( &data, source_id )?,
            )
    }

    fn register_views(&mut self, views: Vec<View>) -> CrimResult<()> {
        for view in views {
            self.views.insert( view.name.clone(), view );
        }
        Ok(())
    }

    pub fn get_view_source(&self, view: &str) -> Option<ViewSource> {
        if let Some(view) = self.views.get(view) {
            if view.source < self.sources.len() {
                Some(self.sources[view.source].clone())
            } else {
                None
            }
        } else {
            None
        }
    }

    pub fn render_partial<T: MappedStructure>(&self, view: &str, input: &T ) -> CrimResult<Context> {
        if let Some(view) = self.views.get(view) {
            return view.process( input )
        } else {
            return Err(CrimError::other("No such view found".into()));
        }
    }

    pub fn render<T: MappedStructure>(&self, view: &str, input: &T ) -> CrimResult<String> {
        if let Some(view) = self.views.get( view ) {
            //println!("::Token tree::\n{:?}", view );
            let mut output = view.process( input )?;
            if let Some(l) = &view.layout {
                self.render( l, &output )
            } else {
                if let Some(content) = output.remove("content") &&
                    let Value::String(s) = content {

                    Ok(s)
                } else {
                    Err(CrimError::other("No content found in root layout.".into()))
                }
            }

        } else {
            Err(CrimError::other("No such view found".into()))
        }
    }
}

fn exec<T: MappedStructure>(input: &StatefulContext<T>, tokens: &Vec<Token>, buf: &mut String) -> CrimResult<()> {
    for token in tokens {
        match token {
            Token::Loop(ident,code) => {
                if let Some(value) = input.get_value( &ident ) &&
                    let Value::List(list) = value {
                    for rec in &list {
                        if let Value::Dictionary(d) = rec {
                            exec( &input.local(d), code, buf )?
                        }
                    }
                }
            }
            Token::If(ident,code,other) => {
                if let Some(value) = input.get_value( &ident ) &&
                    let Value::Bool(b) = value &&
                    b {
                        
                        exec(input, code, buf)?
                } else {
                    exec(input, other, buf)?
                }
            }
            Token::Show(ident,p) => {
                let format = if let Some(s) = p.get("format") {
                    s
                } else {
                    &"".to_string()
                };
                if let Some(value) = input.get_value( &ident ) {
                    match value {
                        Value::Dictionary(_) => {
                            buf.push_str("<dictionary>");
                        }
                        Value::List(_) => {
                            buf.push_str("<list>");
                        }
                        Value::Bool(b) => {
                            if b {
                                buf.push_str("true");
                            } else {
                                buf.push_str("false");
                            }
                        }
                        Value::String(s) => {
                            buf.push_str(s.as_str());
                        }
                        Value::Int(i) => {
                            buf.push_str(&i.to_string());
                        }
                        Value::Float(f) => {
                            if format == "" {
                                buf.push_str(&f.to_string());
                            } else {
                                buf.push_str(&f.to_string());
                            }
                        }
                        Value::Identifier(_) => {
                            // ???
                        }
                    }
                }
            }
            Token::Text(s) => {
                buf.push_str( s );
            }
            _ => {}
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::lexer::*;

    #[test]
    fn lexing() {
        let data = r#"Leading comment: [|view "basic" something="yeup"|>Hello there <|view|] Trailing text"#;
        let ll = lexer::Lexer::new( &data );
        for sym in ll {
            if let SymbolType::Error{what} = sym.symbol() {
                println!("Error {}:{}: {:?}", sym.start().line, sym.start().column, what );
                break;
            } else {
                println!("Symbol: {:?}", sym );
            }
        }
    }

    #[test]
    fn parsing() {
        let mut ct = Crimtag::new();
        if let Err(e) = ct.load_static(r#"Here is a sample
[|view "index" theme="standard"|><html><body>Hi</body></html><|view|]
That was fun! now a placeholder: [|view "placeholder"|] and now one with an implicit [|view "simple"|>Body -> [|show content|] <- and end<|view|] aoeu

[|view "person" layout="simple"|>Welcome [|show person.last_name|], [|show person.first_name|]! It's lovely to see you again.<|view|]

Now with explicit outputs:
[|view "complex"|>
    [|output "content"|>
       Here's a [|show name|].
    <|output|]
    [|output "sidebar"|>
        What's up world?
    <|output|]
    [|output "footer"|>
        Same, I guess!
    <|output|]
<|view|]"#) {
            println!("Error: {:?}", e );
        } else {
            println!("It finished");
            if let Ok(v) = ct.render(
                "person",
                &Context::from([
                    ("hi".to_string(),Value::String("hi".to_string())),
                    ("person".to_string(),Value::Dictionary(HashMap::from([
                        ("first_name".to_string(), Value::String("Bob".to_string())),
                        ("last_name".to_string(), Value::String("Smith".to_string())),
                    ]))),
                    ])
            ) {
                println!("View result: {:?}", v );
            } else {
                println!("Error?");
            }
        }
    }

    #[test]
    fn loops() -> Result<(),CrimError> {
        let mut ct = Crimtag::new();
        ct.load_static(r#"Looping code:
[|view "index"|>We will enumerate people here:[|loop people|>
 - [|show last_name|], [|show first_name|][|if show_title |>: [|show title|] <|else|> **title redacted** <|if|] [|loop tags|> [|show tag|]<|loop|] <|loop|]
<|view|]
"#)?;

        let ctx = Context::from([
            ("people".to_string(), vec![
                [
                    ("first_name".into(), "Joe".into()),
                    ("last_name".into(), "Smith".into()),
                    ("show_title".into(), true.into()),
                    ("title".into(), "CEO".into()),
                ].into(),
                [
                    ("first_name".into(), "Chris".into()),
                    ("last_name".into(), "Perkens".into()),
                    ("show_title".into(), false.into()),
                    ("title".into(), "Baconeer".into()),
                ].into(),
                [
                    ("first_name".into(), "Will".into()),
                    ("last_name".into(), "Power".into()),
                    ("show_title".into(), false.into()),
                    ("title".into(), "CFO".into()),
                ].into(),
                [
                    ("first_name".into(), "Justin".into()),
                    ("last_name".into(), "Time".into()),
                    ("show_title".into(), true.into()),
                    ("title".into(), "CIO".into()),
                ].into(),
            ].into()),
        ]);

        println!("View result: {}", ct.render("index", &ctx)? );

        Ok(())
    }

    #[test]
    fn conditional() -> Result<(),CrimError> {
        let mut ct = Crimtag::new();
        ct.load_static(r#"Hi there
[|view "index"|>
    Color: [|if is_red|> red <|elif is_blue |> blue <|else|> green <|if|]
<|view|]"#)?;

        let ctx = Context::from([
            ("is_red".into(), false.into()),
            ("is_blue".into(), false.into()),
            ("color".into(), "purple".into()),
        ]);
        
        println!("View result: {}", ct.render("index", &ctx)? );

        Ok(())
    }
}
