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;
pub mod context;

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

#[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(&self, input: &dyn for<'a> MappedStructure<'a>) -> 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(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> 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(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> 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<'a>(input: &StatefulContext<'a>, tokens: &Vec<Token>, buf: &mut String) -> CrimResult<()> {
    for token in tokens {
        match token {
            Token::Loop(ident,code) => {
                //println!("!!! Loop over: {:?} {:?}", ident, input.get_value( &ident ));
                if let Some(value) = input.get_value( &ident ) &&
                    let MappedValue::List(list) = value {
                        let output = 
                        &list.for_each(&|rec: &MappedValue, buf: &mut String| {
                            if let MappedValue::Struct(d) = rec {
                                exec( &input.local(*d), code, buf )?;
                            }
                            Ok(())
                        })?;
                        //println!("!!! Output from loop: {}", output );
                    buf.push_str( &output );
                }
            }
            Token::If(ident,code,other) => {
                if let Some(value) = input.get_value( &ident ) &&
                    let MappedValue::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 {
                        MappedValue::Struct(_) => {
                            buf.push_str("Struct");
                        }
                        MappedValue::List(_) => {
                            buf.push_str("List");
                        }
                        MappedValue::Bool(b) => {
                            if b {
                                buf.push_str("true");
                            } else {
                                buf.push_str("false");
                            }
                        }
                        MappedValue::Str(s) => {
                            buf.push_str(s);
                        }
                        MappedValue::String(s) => {
                            buf.push_str(s.as_str());
                        }
                        MappedValue::Int8(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::Int16(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::Int32(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::Int64(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::UInt8(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::UInt16(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::UInt32(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::UInt64(i) => {
                            buf.push_str(&i.to_string());
                        }
                        MappedValue::Float32(f) => {
                            if format == "" {
                                buf.push_str(&f.to_string());
                            } else {
                                buf.push_str(&f.to_string());
                            }
                        }
                        MappedValue::Float64(f) => {
                            if format == "" {
                                buf.push_str(&f.to_string());
                            } else {
                                buf.push_str(&f.to_string());
                            }
                        }
                    }
                }
            }
            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()),
                    ("tags".into(), vec![
                        Context::from([("tag".into(), "jerk".into()),]).into(),
                        Context::from([("tag".into(), "ugly".into()),]).into(),
                    ].into()),
                ].into(),
                [
                    ("first_name".into(), "Chris".into()),
                    ("last_name".into(), "Perkens".into()),
                    ("show_title".into(), false.into()),
                    ("title".into(), "Baconeer".into()),
                    ("tags".into(), vec![
                        Context::from([("tag".into(), "jerk".into()),]).into(),
                        Context::from([("tag".into(), "ugly".into()),]).into(),
                    ].into()),
                ].into(),
                [
                    ("first_name".into(), "Will".into()),
                    ("last_name".into(), "Power".into()),
                    ("show_title".into(), false.into()),
                    ("title".into(), "CFO".into()),
                    ("tags".into(), vec![
                        Context::from([("tag".into(), "jerk".into()),]).into(),
                        Context::from([("tag".into(), "ugly".into()),]).into(),
                    ].into()),
                ].into(),
                [
                    ("first_name".into(), "Justin".into()),
                    ("last_name".into(), "Time".into()),
                    ("show_title".into(), true.into()),
                    ("title".into(), "CIO".into()),
                    ("tags".into(), vec![
                        Context::from([("tag".into(), "jerk".into()),]).into(),
                        Context::from([("tag".into(), "ugly".into()),]).into(),
                    ].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(())
    }

    struct Item {
        pub id: i32,
        pub title: String,
        pub body: String,
    }

    struct Person {
        pub id: i32,
        pub username: String,
        pub name: String,
    }

    struct Page {
        pub user: Person,
        pub total_items: i32,
        pub total_pages: i32,
        pub cur_page: i32,
        pub items: Vec<Item>,
    }

    impl Page {
        fn new() -> Page {
            Page {
                user: Person {
                    id: 443,
                    username: "eichlan".into(),
                    name: "Mike".into(),
                },
                total_items: 4000,
                total_pages: 400,
                cur_page: 35,
                items: vec![
                    Item{
                        id: 123,
                        title: "hi".into(),
                        body: "body".into(),
                    },
                    Item{
                        id: 124,
                        title: "bye".into(),
                        body: "wooo".into(),
                    },
                    Item{
                        id: 125,
                        title: "ciao".into(),
                        body: "whatever".into(),
                    }
                ],
            }
        }
    }

    impl<'a> MappedStructure<'a> for Page {
        fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> {
            match id {
                "user" => Some(MappedValue::Struct(&self.user)),
                "total_items" => Some(MappedValue::Int32(self.total_items)),
                "total_pages" => Some(MappedValue::Int32(self.total_pages)),
                "cur_page" => Some(MappedValue::Int32(self.cur_page)),
                "items" => Some(MappedValue::<'a>::List(&self.items)),
                _ => None,
            }
        }
    }
    
    impl<'a> MappedStructure<'a> for Person {
        fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> {
            match id {
                "id" => Some(MappedValue::Int32(self.id)),
                "username" => Some(MappedValue::String(&self.username)),
                "name" => Some(MappedValue::String(&self.name)),
                _ => None,
            }
        }
    }
    
    impl<'a> MappedStructure<'a> for Item {
        fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> {
            match id {
                "id" => Some(MappedValue::Int32(self.id)),
                "title" => Some(MappedValue::String(&self.title)),
                "body" => Some(MappedValue::String(&self.body)),
                _ => None,
            }
        }
    }
    
    #[test]
    fn custom() -> Result<(), CrimError> {
        let page = Page::new();

        println!("{:?}",
            page.get_value(&"total_pages")
        );

        if let Some(MappedValue::List(l)) = page.get_value(&"items") {
            l.for_each(&|x: &MappedValue, buf: &mut String| {
                if let MappedValue::Struct(s) = x {
                    println!(" - {:?}", s.get_value(&"title"));
                    buf.push_str(&format!("{:?}",s.get_value(&"title")));
                }
                Ok(())
            }).expect("loop");
        }
        Ok(())
    }
}
