// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use super::*;
use crate::parsing::text_query::{parse_expr, Token, TokenIterator};
use serde_derive::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::ops::Deref;
use std::rc::Rc;

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub enum DateMatch {
    Day,
    Era,
    FullDay,
    FullHour12,
    FullHour24,
    FullYear,
    Hour12,
    Hour24,
    IsoWeek,
    IsoYear,
    Meridiem,
    Min,
    MonthName,
    MonthNum,
    Offset,
    Ordinal,
    Sec,
    WeekDay,
    Year,
    Today,
    Now,
    Relative,
}

impl DateMatch {
    pub fn from_str(input: &str) -> Option<DateMatch> {
        match input {
            "day" => Some(DateMatch::Day),
            "adbc" => Some(DateMatch::Era),
            "fullday" => Some(DateMatch::FullDay),
            "fullhour12" => Some(DateMatch::FullHour12),
            "fullhour24" => Some(DateMatch::FullHour24),
            "fullyear" => Some(DateMatch::FullYear),
            "hour12" => Some(DateMatch::Hour12),
            "hour24" => Some(DateMatch::Hour24),
            "isoweek" => Some(DateMatch::IsoWeek),
            "isoyear" => Some(DateMatch::IsoYear),
            "meridiem" => Some(DateMatch::Meridiem),
            "min" => Some(DateMatch::Min),
            "monthname" => Some(DateMatch::MonthName),
            "monthnum" => Some(DateMatch::MonthNum),
            "offset" => Some(DateMatch::Offset),
            "ordinal" => Some(DateMatch::Ordinal),
            "sec" => Some(DateMatch::Sec),
            "weekday" => Some(DateMatch::WeekDay),
            "year" => Some(DateMatch::Year),
            "today" => Some(DateMatch::Today),
            "now" => Some(DateMatch::Now),
            "relative" => Some(DateMatch::Relative),
            _ => None,
        }
    }

    pub fn name(self) -> &'static str {
        match self {
            DateMatch::Day => "day",
            DateMatch::Era => "adbc",
            DateMatch::FullDay => "fullday",
            DateMatch::FullHour12 => "fullhour12",
            DateMatch::FullHour24 => "fullhour24",
            DateMatch::FullYear => "fullyear",
            DateMatch::Hour12 => "hour12",
            DateMatch::Hour24 => "hour24",
            DateMatch::IsoWeek => "isoweek",
            DateMatch::IsoYear => "isoyear",
            DateMatch::Meridiem => "meridiem",
            DateMatch::Min => "min",
            DateMatch::MonthName => "monthname",
            DateMatch::MonthNum => "monthnum",
            DateMatch::Offset => "offset",
            DateMatch::Ordinal => "ordinal",
            DateMatch::Sec => "sec",
            DateMatch::WeekDay => "weekday",
            DateMatch::Year => "year",
            DateMatch::Today => "today",
            DateMatch::Now => "now",
            DateMatch::Relative => "relative",
        }
    }
}

impl fmt::Display for DateMatch {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{}", self.name())
    }
}

#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum DatePattern {
    Literal(String),
    Match(DateMatch),
    Optional(Vec<DatePattern>),
    Dash,
    Colon,
    Space,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(into = "String", try_from = "String")]
pub struct ExprString(pub Expr);

impl From<ExprString> for String {
    fn from(value: ExprString) -> String {
        format!("{}", value.0)
    }
}

impl TryFrom<String> for ExprString {
    type Error = String;

    fn try_from(input: String) -> Result<Self, Self::Error> {
        let mut iter = TokenIterator::new(&input).peekable();
        let expr = parse_expr(&mut iter);
        if let Some(Token::Eof) = iter.next() {
            Ok(ExprString(expr))
        } else {
            Err("Expected EOF".to_owned())
        }
    }
}

impl Deref for ExprString {
    type Target = Expr;

    fn deref(&self) -> &Expr {
        &self.0
    }
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Property {
    pub name: String,
    pub input: ExprString,
    pub input_name: String,
    pub output: ExprString,
    pub output_name: String,
    pub doc: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum Def {
    BaseUnit {
        #[serde(rename = "longName")]
        long_name: Option<String>,
    },
    Prefix {
        expr: ExprString,
        #[serde(rename = "isLong")]
        is_long: bool,
    },
    Unit {
        expr: ExprString,
    },
    Quantity {
        expr: ExprString,
    },
    Substance {
        symbol: Option<String>,
        properties: Vec<Property>,
    },
    Category {
        #[serde(rename = "displayName")]
        display_name: String,
    },
    Dependency {
        name: String,
    },
    Error {
        message: String,
    },
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct DefEntry {
    pub name: String,
    #[serde(flatten)]
    pub def: Rc<Def>,
    pub doc: Option<String>,
    pub category: Option<String>,
}

impl DefEntry {
    pub fn new(name: &str, doc: Option<&str>, category: Option<&str>, def: Def) -> DefEntry {
        DefEntry {
            name: name.into(),
            doc: doc.map(Into::into),
            category: category.map(Into::into),
            def: Rc::new(def),
        }
    }

    pub fn new_unit(name: &str, doc: Option<&str>, category: Option<&str>, expr: Expr) -> DefEntry {
        Self::new(
            name,
            doc,
            category,
            Def::Unit {
                expr: ExprString(expr),
            },
        )
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Defs {
    pub defs: Vec<DefEntry>,
}

impl fmt::Display for DatePattern {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            DatePattern::Literal(ref l) => write!(fmt, "'{}'", l),
            DatePattern::Match(ref n) => write!(fmt, "{}", n),
            DatePattern::Optional(ref pats) => {
                write!(fmt, "[")?;
                for p in pats {
                    p.fmt(fmt)?;
                }
                write!(fmt, "]")
            }
            DatePattern::Dash => write!(fmt, "-"),
            DatePattern::Colon => write!(fmt, ":"),
            DatePattern::Space => write!(fmt, " "),
        }
    }
}

impl DatePattern {
    pub fn show(pat: &[DatePattern]) -> String {
        use std::io::Write;

        let mut buf = vec![];
        for p in pat {
            write!(buf, "{}", p).unwrap();
        }
        String::from_utf8(buf).unwrap()
    }
}
