use std::collections::BTreeSet;

pub use pest_consume::{match_nodes, Error, Parser};

use TodoBodyToken::*;

// TODO: support arbitrary kv in todos e.g., foo:xyz?
// TODO: dependent tasks, nanoids //
// https://cdn.rawgit.com/bram85/topydo/master/docs/index.html#Dependencies
// #[derive(Eq, PartialEq, Debug)]
// enum ID {
//     // these are nano IDs
//     ID(String),
// }

#[derive(Eq, PartialEq, Debug, Clone)]
enum TodoBodyToken {
    Word(String),
    Context(String),
    Project(String),
    ThresholdDate(time::Date),
    DueDate(time::Date),
    Hidden(bool),
}

#[derive(Eq, PartialEq, Debug)]
pub struct Todo {
    pub is_completed: bool,
    pub priority: Option<char>,
    pub creation_date: Option<time::Date>,
    pub completion_date: Option<time::Date>,
    tokens: Vec<TodoBodyToken>,
    pub threshold_date: Option<time::Date>,
    pub due_date: Option<time::Date>,
    pub contexts: BTreeSet<String>,
    pub projects: BTreeSet<String>,
    pub is_hidden: bool,
    // TODO: dependent tasks + NANO IDs
    // https://cdn.rawgit.com/bram85/topydo/master/docs/index.html#Dependencies
    // is_blocking_for: Vec<ID>,
    // is_blocked: bool,
    // id: Option<ID>,
}

#[allow(dead_code)]
impl Todo {
    pub fn has_context(&self, context: &str) -> bool {
        if !context.starts_with('@') {
            let c = "@".to_string() + context;
            return self.has_context(&c);
        }
        self.contexts.contains(context)
    }

    pub fn has_project(&self, project: &str) -> bool {
        if !project.starts_with('+') {
            let c = "+".to_string() + project;
            return self.has_project(&c);
        }
        self.projects.contains(project)
    }

    pub fn canonical_context(&self) -> Option<String> {
        self.contexts.first().map(|s| s.to_owned())
    }

    pub fn canonical_project(&self) -> Option<String> {
        self.projects.first().map(|s| s.to_owned())
    }

    pub fn parse(input: &str) -> std::result::Result<Todo, TodoParserError> {
        let input = input.trim();
        if input.is_empty() {
            return Err(TodoParserError::EmptyInput);
        }
        let nodes = TodoParser::parse(Rule::todo, input)?;
        let node = nodes.single()?;
        let todo = TodoParser::todo(node)?;
        Ok(todo)
    }
}

#[derive(Debug)]
struct TodoBuilder {
    is_completed: bool,
    priority: Option<char>,
    creation_date: Option<time::Date>,
    completion_date: Option<time::Date>,
    tokens: Vec<TodoBodyToken>,
    threshold_date: Option<time::Date>,
    due_date: Option<time::Date>,
    // TODO: make it a BTreeSet<&str>
    contexts: BTreeSet<String>,
    // TODO: make it a BTreeSet<&str>
    projects: BTreeSet<String>,
    is_hidden: bool,
}

impl TodoBuilder {
    pub fn default() -> Self {
        TodoBuilder {
            is_completed: false,
            priority: None,
            creation_date: None,
            completion_date: None,
            tokens: Vec::new(),
            threshold_date: None,
            due_date: None,
            contexts: BTreeSet::new(),
            projects: BTreeSet::new(),
            is_hidden: false,
        }
    }
    pub fn completed(&mut self, completed: bool) -> &mut Self {
        self.is_completed = completed;
        self
    }

    pub fn priority(&mut self, priority: char) -> &mut Self {
        // TODO: return Result<>, check prio char
        self.priority = Some(priority);
        self
    }

    pub fn creation_date(&mut self, creation_date: time::Date) -> &mut Self {
        self.creation_date = Some(creation_date);
        self
    }

    pub fn completion_date(&mut self, completion_date: time::Date) -> &mut Self {
        self.is_completed = true;
        self.completion_date = Some(completion_date);
        self
    }

    pub fn tokens(&mut self, tokens: Vec<TodoBodyToken>) -> &mut Self {
        for t in tokens {
            &self.push_token(t);
        }
        self
    }

    pub fn push_token(&mut self, tkn: TodoBodyToken) {
        self.tokens.push(tkn.clone());
        match tkn {
            TodoBodyToken::Context(s) => {
                self.contexts.insert(s); // TODO: store a reference instead
            }
            TodoBodyToken::Project(s) => {
                self.projects.insert(s); // TODO: store a reference instead
            }
            TodoBodyToken::ThresholdDate(td) => {
                self.threshold_date = Some(td); // TODO: store a reference instead
            }
            TodoBodyToken::DueDate(d) => {
                self.due_date = Some(d); // TODO: store a reference instead
            }
            TodoBodyToken::Hidden(b) => {
                self.is_hidden = b;
            }
            TodoBodyToken::Word(_) => {}
        }
    }

    pub fn build(&mut self) -> Todo {
        let tokens = std::mem::replace(&mut self.tokens, vec![]);
        let contexts = std::mem::replace(&mut self.contexts, BTreeSet::new());
        let projects = std::mem::replace(&mut self.projects, BTreeSet::new());
        Todo {
            is_completed: self.is_completed,
            priority: self.priority,
            creation_date: self.creation_date,
            completion_date: self.completion_date,
            tokens,
            threshold_date: self.threshold_date,
            due_date: self.due_date,
            contexts,
            projects,
            is_hidden: self.is_hidden,
        }
    }
}

#[derive(Debug)]
struct TodoBodyTokensBuilder {
    tokens: Vec<TodoBodyToken>,
}

impl TodoBodyTokensBuilder {
    fn default() -> TodoBodyTokensBuilder {
        TodoBodyTokensBuilder { tokens: Vec::new() }
    }
    fn word(mut self, s: &str) -> TodoBodyTokensBuilder {
        self.tokens.push(Word(s.to_string()));
        self
    }
    fn project(mut self, s: &str) -> TodoBodyTokensBuilder {
        self.tokens.push(Project(s.to_string()));
        self
    }
    fn context(mut self, s: &str) -> TodoBodyTokensBuilder {
        self.tokens.push(Context(s.to_string()));
        self
    }
    fn threshold_date(mut self, d: time::Date) -> TodoBodyTokensBuilder {
        self.tokens.push(ThresholdDate(d));
        self
    }
    fn due_date(mut self, d: time::Date) -> TodoBodyTokensBuilder {
        self.tokens.push(DueDate(d));
        self
    }
    fn build(self) -> Vec<TodoBodyToken> {
        self.tokens
    }
}

#[derive(Parser)]
#[grammar = "./todotxt.pest"]
struct TodoParser;

type PestResult<T> = std::result::Result<T, pest_consume::Error<Rule>>;
type Node<'i> = pest_consume::Node<'i, Rule, ()>;

fn parse_date(node: Node, input: &str) -> PestResult<time::Date> {
    time::Date::parse(input, "%F").map_err(|e| node.error(e))
}

#[pest_consume::parser]
impl TodoParser {
    fn completion(_input: Node) -> PestResult<bool> {
        Ok(true)
    }

    fn priority(input: Node) -> PestResult<char> {
        // i'm pretty sure this is safe for `ASCII_ALPHA` priorities as defined in the grammar...
        let mut chars = input.as_str().chars();
        chars.next();
        let pri = chars.next().unwrap().to_uppercase().next().unwrap();
        Ok(pri)
    }

    fn date(input: Node) -> PestResult<time::Date> {
        let s = input.as_str();
        Ok(parse_date(input, s)?)
    }

    fn creation_date(input: Node) -> PestResult<time::Date> {
        TodoParser::date(input.children().single()?)
    }

    fn completion_date(input: Node) -> PestResult<time::Date> {
        TodoParser::date(input.children().single()?)
    }

    fn context(input: Node) -> PestResult<TodoBodyToken> {
        Ok(Context(input.as_str().to_string()))
    }

    fn project(input: Node) -> PestResult<TodoBodyToken> {
        Ok(Project(input.as_str().to_string()))
    }

    fn threshold_date(input: Node) -> PestResult<TodoBodyToken> {
        let mut chars = input.as_str().chars();
        chars.next(); // t
        chars.next(); // :
        let d = parse_date(input, chars.as_str())?;
        Ok(ThresholdDate(d))
    }

    fn due_date(input: Node) -> PestResult<TodoBodyToken> {
        let mut chars = input.as_str().chars();
        chars.next(); // d
        chars.next(); // u
        chars.next(); // e
        chars.next(); // :
        let d = parse_date(input, chars.as_str())?;
        Ok(DueDate(d))
    }

    fn hidden(input: Node) -> PestResult<TodoBodyToken> {
        let mut chars = input.as_str().chars();
        chars.next(); // h
        chars.next(); // :
        let v = chars.next().unwrap(); // 1 | 0
        let b = if v == '1' {
            true
        } else if v == '0' {
            false
        } else {
            unreachable!();
        };
        Ok(Hidden(b))
    }

    fn word(input: Node) -> PestResult<TodoBodyToken> {
        Ok(Word(input.as_str().to_string()))
    }

    fn token(input: Node) -> PestResult<TodoBodyToken> {
        let t = match_nodes!(input.into_children();
            [context(t)] => t,
            [project(t)] => t,
            [threshold_date(t)] => t,
            [due_date(t)] => t,
            [hidden(t)] => t,
            [word(t)] => t,
        );
        Ok(t)
    }

    fn todo(node: Node) -> PestResult<Todo> {
        let mut tb = TodoBuilder::default();
        match_nodes!(node.into_children();
        // 4 choose k, tokens always there since it can be empty
        // 4 choose 4 = 1
        [completion(c), priority(p), creation_date(crd), completion_date(cod), token(tkns)..]
            => {
                    tb.completed(c);
                    tb.priority(p);
                    tb.creation_date(crd);
                    tb.completion_date(cod);
                    tb.tokens(tkns.collect());
               },
        // 4 choose 3 = 4
        [priority(p), creation_date(crd), completion_date(cod), token(tkns)..]
            => {
                    tb.priority(p);
                    tb.creation_date(crd);
                    tb.tokens(tkns.collect());
                    log::debug!("ignoring completion date {:?} from incomplete todo {:?}", cod, tb)
               },
        [completion(c), creation_date(crd), completion_date(cod), token(tkns)..]
            => {
                    tb.completed(c);
                    tb.creation_date(crd);
                    tb.completion_date(cod);
                    tb.tokens(tkns.collect());
               },
        [completion(c), priority(p), completion_date(cod), token(tkns)..]
            => {
                    // should never match, creation_date is greedier
                    unreachable!()
               },
        [completion(c), priority(p), creation_date(crd), token(tkns)..]
            => {
                    tb.completed(c);
                    tb.priority(p);
                    tb.creation_date(crd);
                    tb.tokens(tkns.collect());
               },
        // 4 choose 2 = 6
        [creation_date(crd), completion_date(cod), token(tkns)..]
            => {
                    tb.creation_date(crd);
                    tb.tokens(tkns.collect());
                    log::debug!("ignoring completion date {:?} from incomplete todo {:?}", cod, tb)
               },
        [priority(p), completion_date(cod), token(tkns)..]
            => {
                    // should never match, creation_date is greedier
                    unreachable!()
               },
        [priority(p), creation_date(crd), token(tkns)..]
            => {
                    tb.priority(p);
                    tb.creation_date(crd);
                    tb.tokens(tkns.collect());
               },
        [completion(c), completion_date(cod), token(tkns)..]
            => {
                    // should never match, creation_date is greedier
                    unreachable!()
               },
        [completion(c), creation_date(crd), token(tkns)..]
            => {
                    tb.completed(c);
                    tb.creation_date(crd);
                    tb.tokens(tkns.collect());
               },
        [completion(c), priority(p), token(tkns)..]
            => {
                    tb.completed(c);
                    tb.priority(p);
                    tb.tokens(tkns.collect());
               },
        // 4 choose 1 = 4
        [completion(c), token(tkns)..]
            => {
                    tb.completed(c);
                    tb.tokens(tkns.collect());
                },
        [priority(p), token(tkns)..]
            => {
                    tb.priority(p);
                    tb.tokens(tkns.collect());
                },
        [completion_date(cod), token(tkns)..]
            => {
                    // should never match, creation_date is greedier
                    unreachable!()
               },
        [creation_date(crd), token(tkns)..]
            => {
                    tb.creation_date(crd);
                    tb.tokens(tkns.collect());
                },
        [token(tkns)..]
            => {
                    tb.tokens(tkns.collect());
                },
        );

        Ok(tb.build())
    }
}

#[derive(Debug)]
pub enum TodoParserError {
    EmptyInput,
    ParseError(pest_consume::Error<Rule>),
}

impl std::fmt::Display for TodoParserError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl std::error::Error for TodoParserError {}

impl std::convert::From<pest_consume::Error<Rule>> for TodoParserError {
    fn from(e: pest_consume::Error<Rule>) -> Self {
        TodoParserError::ParseError(e)
    }
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use crate::utils;

    use super::*;

    fn parse_node<T>(
        root_rule: Rule,
        visitor: fn(Node) -> PestResult<T>,
        input: &str,
    ) -> PestResult<T> {
        let nodes = TodoParser::parse(root_rule, input)?;
        let node = nodes.single()?;
        visitor(node)
    }

    fn parse_token(input: &str) -> PestResult<TodoBodyToken> {
        parse_node(Rule::token, TodoParser::token, input)
    }

    #[test]
    fn parse_intermediate_parsers_work_ok() {
        utils::test::init();
        assert_eq!(
            Ok(true),
            parse_node(Rule::completion, TodoParser::completion, "x")
        );

        assert_eq!(
            Ok('D'),
            parse_node(Rule::priority, TodoParser::priority, "(D)")
        );
        // assert_eq!(Ok('C'), parse_node(Rule::priority, TodoParser::priority, "( c)" )); // FIXME:

        assert_eq!(
            Ok(time::date!(2020 - 12 - 13)),
            parse_node(Rule::date, TodoParser::date, "2020-12-13")
        );
        assert_eq!(
            Ok(time::date!(2020 - 12 - 13)),
            parse_node(Rule::creation_date, TodoParser::creation_date, "2020-12-13")
        );
        assert_eq!(
            Ok(time::date!(2020 - 02 - 29)),
            parse_node(
                Rule::completion_date,
                TodoParser::completion_date,
                "2020-02-29",
            )
        );

        assert_eq!(
            Ok(TodoBodyToken::Context("@foo".to_string())),
            parse_node(Rule::context, TodoParser::context, "@foo")
        );
        assert_eq!(
            Ok(TodoBodyToken::Project("+foo".to_string())),
            parse_node(Rule::project, TodoParser::project, "+foo")
        );
        assert_eq!(
            Ok(TodoBodyToken::ThresholdDate(time::date!(2020 - 11 - 14))),
            parse_node(
                Rule::threshold_date,
                TodoParser::threshold_date,
                "t:2020-11-14",
            )
        );
        assert_eq!(
            Ok(TodoBodyToken::DueDate(time::date!(2020 - 04 - 04))),
            parse_node(Rule::due_date, TodoParser::due_date, "due:2020-04-04")
        );
        assert_eq!(
            Ok(TodoBodyToken::Hidden(true)),
            parse_node(Rule::hidden, TodoParser::hidden, "h:1")
        );
        assert_eq!(
            Ok(TodoBodyToken::Hidden(false)),
            parse_node(Rule::hidden, TodoParser::hidden, "h:0")
        );
        assert_eq!(
            Ok(TodoBodyToken::Word("hi".to_string())),
            parse_node(Rule::word, TodoParser::word, "hi")
        );

        assert_eq!(
            Ok(TodoBodyToken::Context("@foo".to_string())),
            parse_token("@foo")
        );
        assert_eq!(
            Ok(TodoBodyToken::Project("+foo".to_string())),
            parse_token("+foo")
        );
        assert_eq!(
            Ok(TodoBodyToken::ThresholdDate(time::date!(2020 - 11 - 14))),
            parse_token("t:2020-11-14")
        );
        assert_eq!(
            Ok(TodoBodyToken::DueDate(time::date!(2020 - 04 - 04))),
            parse_token("due:2020-04-04")
        );
        assert_eq!(Ok(TodoBodyToken::Hidden(true)), parse_token("h:1"));
        assert_eq!(Ok(TodoBodyToken::Hidden(false)), parse_token("h:0"));
        assert_eq!(Ok(TodoBodyToken::Word("hi".to_string())), parse_token("hi"));
    }

    #[test]
    fn parsing_a_todo() {
        utils::test::init();
        let got = Todo::parse(
            "x (A) 2020-10-10 2020-12-29 write +tudor Todo parser @pc t:2020-12-25 due:2020-12-30)",
        )
        .unwrap();
        let exp = TodoBuilder::default()
            .completed(true)
            .priority('A')
            .creation_date(time::date!(2020 - 10 - 10))
            .completion_date(time::date!(2020 - 12 - 29))
            .tokens(
                TodoBodyTokensBuilder::default()
                    .word("write")
                    .project("+tudor")
                    .word("Todo")
                    .word("parser")
                    .context("@pc")
                    .threshold_date(time::date!(2020 - 12 - 25))
                    .due_date(time::date!(2020 - 12 - 30))
                    .build(),
            )
            .build();
        assert_eq!(got, exp);
    }

    #[test]
    fn parsing_a_todo_with_ws() {
        utils::test::init();
        let got = Todo::parse(
            "  x   (B)   2020-10-10   2020-12-29   parse     todos with whitespace +tudor @pc t:2020-12-25 due:2020-12-31)    ",
        ).unwrap();
        let exp = TodoBuilder::default()
            .completed(true)
            .priority('B')
            .creation_date(time::date!(2020 - 10 - 10))
            .completion_date(time::date!(2020 - 12 - 29))
            .tokens(
                TodoBodyTokensBuilder::default()
                    .word("parse")
                    .word("todos")
                    .word("with")
                    .word("whitespace")
                    .project("+tudor")
                    .context("@pc")
                    .threshold_date(time::date!(2020 - 12 - 25))
                    .due_date(time::date!(2020 - 12 - 31))
                    .build(),
            )
            .build();
        assert_eq!(got, exp);
    }

    #[test]
    fn parsing_a_todo_with_body_only() {
        utils::test::init();
        let got = Todo::parse("try out pest @pc").unwrap();
        let exp = TodoBuilder::default()
            .tokens(
                TodoBodyTokensBuilder::default()
                    .word("try")
                    .word("out")
                    .word("pest")
                    .context("@pc")
                    .build(),
            )
            .build();
        assert_eq!(got, exp);
    }

    #[test]
    fn parsing_some_other_todos() {
        utils::test::init();
        assert!(Todo::parse("(A)").is_ok());
        // is invalid but we parse it (incomplete task with completion date)
        assert!(Todo::parse("(S) 1029-12-19 1029-12-20 try out pest @pc").is_ok());
        assert!(Todo::parse("(S) 1029-12-19 try out pest @pc").is_ok());
        assert!(Todo::parse("(S) try out pest @pc").is_ok());
        assert!(Todo::parse("1029-12-19 token token").is_ok());
        assert!(Todo::parse("1029-12-19").is_ok());
        // is invalid, but we parse it (incomplete task with completion date)
        assert!(Todo::parse("1039-12-12 1999-12-12 try out pest @pc").is_ok());
        assert!(Todo::parse("1039-12-12 try out pest @pc").is_ok());
        assert!(Todo::parse("x (A) 1029-12-20 token token").is_ok());
        assert!(Todo::parse("x (S) 1029-12-19 1029-12-19 try out pest @pc").is_ok());
        assert!(Todo::parse("x (S) 1029-12-19 try out pest @pc").is_ok());
        assert!(Todo::parse("x (S) try out pest @pc").is_ok());
        assert!(Todo::parse("x (S)").is_ok());
        assert!(Todo::parse("x 1029-12-19 1029-12-20 token token").is_ok());
        assert!(Todo::parse("x 1039-12-12 try out pest @pc").is_ok());
        assert!(Todo::parse("x try out pest @pc").is_ok());
        assert!(Todo::parse("x").is_ok());
    }
}
