#![allow(clippy::type_complexity)]
use crate::parse::{call_node::*, comment::*, flag::*, number::*, operator::*, pipeline::*};
use derive_new::new;
use getset::Getters;
use nu_errors::{ParseError, ShellError};
use nu_protocol::{ShellTypeName, SpannedTypeName};
use nu_source::{
    b, DebugDocBuilder, HasSpan, PrettyDebugWithSource, Span, Spanned, SpannedItem, Text,
};
use std::borrow::Cow;
use std::ops::Deref;

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Token {
    Number(RawNumber),
    CompareOperator(CompareOperator),
    EvaluationOperator(EvaluationOperator),
    String(Span),
    Variable(Span),
    ItVariable(Span),
    ExternalCommand(Span),
    ExternalWord,
    GlobPattern,
    Bare,
    Garbage,

    Call(CallNode),
    Delimited(DelimitedNode),
    Pipeline(Pipeline),
    Flag(Flag),
    Comment(Comment),
    Whitespace,
    Separator,
}

macro_rules! token_type {
    (struct $name:tt (desc: $desc:tt) -> $out:ty { |$span:ident, $pat:pat| => $do:expr }) => {
        pub struct $name;

        impl TokenType for $name {
            type Output = $out;

            fn desc(&self) -> Cow<'static, str> {
                Cow::Borrowed($desc)
            }

            fn extract_token_value(
                &self,
                token: &SpannedToken,
                err: ParseErrorFn<$out>,
            ) -> Result<$out, ParseError> {
                let $span = token.span();

                match *token.unspanned() {
                    $pat => Ok($do),
                    _ => err(),
                }
            }
        }
    };

    (struct $name:tt (desc: $desc:tt) -> $out:ty { $pat:pat => $do:expr }) => {
        pub struct $name;

        impl TokenType for $name {
            type Output = $out;

            fn desc(&self) -> Cow<'static, str> {
                Cow::Borrowed($desc)
            }

            fn extract_token_value(
                &self,
                token: &SpannedToken,
                err: ParseErrorFn<$out>,
            ) -> Result<$out, ParseError> {
                match token.unspanned().clone() {
                    $pat => Ok($do),
                    _ => err(),
                }
            }
        }
    };
}

pub type ParseErrorFn<'a, T> = &'a dyn Fn() -> Result<T, ParseError>;

token_type!(struct IntType (desc: "integer") -> RawNumber {
    Token::Number(number @ RawNumber::Int(_)) => number
});

token_type!(struct DecimalType (desc: "decimal") -> RawNumber {
    Token::Number(number @ RawNumber::Decimal(_)) => number
});

token_type!(struct StringType (desc: "string") -> (Span, Span) {
    |outer, Token::String(inner)| => (inner, outer)
});

token_type!(struct BareType (desc: "word") -> Span {
    |span, Token::Bare| => span
});

token_type!(struct DotType (desc: "dot") -> Span {
    |span, Token::EvaluationOperator(EvaluationOperator::Dot)| => span
});

token_type!(struct DotDotType (desc: "dotdot") -> Span {
    |span, Token::EvaluationOperator(EvaluationOperator::DotDot)| => span
});

token_type!(struct CompareOperatorType (desc: "compare operator") -> (Span, CompareOperator) {
    |span, Token::CompareOperator(operator)| => (span, operator)
});

token_type!(struct ExternalWordType (desc: "external word") -> Span {
    |span, Token::ExternalWord| => span
});

token_type!(struct ExternalCommandType (desc: "external command") -> (Span, Span) {
    |outer, Token::ExternalCommand(inner)| => (inner, outer)
});

token_type!(struct CommentType (desc: "comment") -> (Comment, Span) {
    |outer, Token::Comment(comment)| => (comment, outer)
});

token_type!(struct SeparatorType (desc: "separator") -> Span {
    |span, Token::Separator| => span
});

token_type!(struct WhitespaceType (desc: "whitespace") -> Span {
    |span, Token::Whitespace| => span
});

token_type!(struct WordType (desc: "word") -> Span {
    |span, Token::Bare| => span
});

token_type!(struct ItVarType (desc: "$it") -> (Span, Span) {
    |outer, Token::ItVariable(inner)| => (inner, outer)
});

token_type!(struct VarType (desc: "variable") -> (Span, Span) {
    |outer, Token::Variable(inner)| => (inner, outer)
});

token_type!(struct PipelineType (desc: "pipeline") -> Pipeline {
    Token::Pipeline(pipeline) => pipeline
});

token_type!(struct BlockType (desc: "block") -> DelimitedNode {
    Token::Delimited(block @ DelimitedNode { delimiter: Delimiter::Brace, .. }) => block
});

token_type!(struct SquareType (desc: "square") -> DelimitedNode {
    Token::Delimited(square @ DelimitedNode { delimiter: Delimiter::Square, .. }) => square
});

pub trait TokenType {
    type Output;

    fn desc(&self) -> Cow<'static, str>;

    fn extract_token_value(
        &self,
        token: &SpannedToken,
        err: ParseErrorFn<Self::Output>,
    ) -> Result<Self::Output, ParseError>;
}

impl Token {
    pub fn into_spanned(self, span: impl Into<Span>) -> SpannedToken {
        SpannedToken {
            unspanned: self,
            span: span.into(),
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct SpannedToken {
    #[get = "pub"]
    unspanned: Token,
    span: Span,
}

impl Deref for SpannedToken {
    type Target = Token;
    fn deref(&self) -> &Self::Target {
        &self.unspanned
    }
}

impl HasSpan for SpannedToken {
    fn span(&self) -> Span {
        self.span
    }
}

impl ShellTypeName for SpannedToken {
    fn type_name(&self) -> &'static str {
        self.unspanned.type_name()
    }
}

impl PrettyDebugWithSource for SpannedToken {
    fn pretty_debug(&self, source: &str) -> DebugDocBuilder {
        match self.unspanned() {
            Token::Number(number) => number.pretty_debug(source),
            Token::CompareOperator(operator) => operator.pretty_debug(source),
            Token::EvaluationOperator(operator) => operator.pretty_debug(source),
            Token::String(_) | Token::GlobPattern | Token::Bare => {
                b::primitive(self.span.slice(source))
            }
            Token::Variable(_) => b::var(self.span.slice(source)),
            Token::ItVariable(_) => b::keyword(self.span.slice(source)),
            Token::ExternalCommand(_) => b::description(self.span.slice(source)),
            Token::ExternalWord => b::description(self.span.slice(source)),
            Token::Call(call) => call.pretty_debug(source),
            Token::Delimited(delimited) => delimited.pretty_debug(source),
            Token::Pipeline(pipeline) => pipeline.pretty_debug(source),
            Token::Flag(flag) => flag.pretty_debug(source),
            Token::Garbage => b::error(self.span.slice(source)),
            Token::Whitespace => b::typed(
                "whitespace",
                b::description(format!("{:?}", self.span.slice(source))),
            ),
            Token::Separator => b::typed(
                "separator",
                b::description(format!("{:?}", self.span.slice(source))),
            ),
            Token::Comment(comment) => {
                b::typed("comment", b::description(comment.text.slice(source)))
            }
        }
    }
}

impl ShellTypeName for Token {
    fn type_name(&self) -> &'static str {
        match self {
            Token::Number(_) => "number",
            Token::CompareOperator(_) => "comparison operator",
            Token::EvaluationOperator(EvaluationOperator::Dot) => "dot",
            Token::EvaluationOperator(EvaluationOperator::DotDot) => "dot dot",
            Token::String(_) => "string",
            Token::Variable(_) => "variable",
            Token::ItVariable(_) => "it variable",
            Token::ExternalCommand(_) => "external command",
            Token::ExternalWord => "external word",
            Token::GlobPattern => "glob pattern",
            Token::Bare => "word",
            Token::Call(_) => "command",
            Token::Delimited(d) => d.type_name(),
            Token::Pipeline(_) => "pipeline",
            Token::Flag(_) => "flag",
            Token::Garbage => "garbage",
            Token::Whitespace => "whitespace",
            Token::Separator => "separator",
            Token::Comment(_) => "comment",
        }
    }
}

impl From<&SpannedToken> for Span {
    fn from(token: &SpannedToken) -> Span {
        token.span
    }
}

impl SpannedToken {
    pub fn as_external_arg(&self, source: &Text) -> String {
        self.span().slice(source).to_string()
    }

    pub fn source<'a>(&self, source: &'a Text) -> &'a str {
        self.span().slice(source)
    }

    pub fn get_variable(&self) -> Result<(Span, Span), ShellError> {
        match self.unspanned() {
            Token::Variable(inner_span) => Ok((self.span(), *inner_span)),
            _ => Err(ShellError::type_error("variable", self.spanned_type_name())),
        }
    }

    pub fn is_bare(&self) -> bool {
        match self.unspanned() {
            Token::Bare => true,
            _ => false,
        }
    }

    pub fn is_string(&self) -> bool {
        match self.unspanned() {
            Token::String(_) => true,
            _ => false,
        }
    }

    pub fn is_number(&self) -> bool {
        match self.unspanned() {
            Token::Number(_) => true,
            _ => false,
        }
    }

    pub fn is_int(&self) -> bool {
        match self.unspanned() {
            Token::Number(RawNumber::Int(_)) => true,
            _ => false,
        }
    }

    pub fn as_string(&self) -> Option<(Span, Span)> {
        match self.unspanned() {
            Token::String(inner_span) => Some((self.span(), *inner_span)),
            _ => None,
        }
    }

    pub fn is_pattern(&self) -> bool {
        match self.unspanned() {
            Token::GlobPattern => true,
            _ => false,
        }
    }

    pub fn is_word(&self) -> bool {
        match self.unspanned() {
            Token::Bare => true,
            _ => false,
        }
    }

    pub fn is_dot(&self) -> bool {
        match self.unspanned() {
            Token::EvaluationOperator(EvaluationOperator::Dot) => true,
            _ => false,
        }
    }

    pub fn is_separator(&self) -> bool {
        match self.unspanned() {
            Token::Separator => true,
            _ => false,
        }
    }

    pub fn as_block(&self) -> Option<(Spanned<&[SpannedToken]>, (Span, Span))> {
        match self.unspanned() {
            Token::Delimited(DelimitedNode {
                delimiter,
                children,
                spans,
            }) if *delimiter == Delimiter::Brace => {
                Some(((&children[..]).spanned(self.span()), *spans))
            }
            _ => None,
        }
    }

    pub fn is_external(&self) -> bool {
        match self.unspanned() {
            Token::ExternalCommand(..) => true,
            _ => false,
        }
    }

    pub(crate) fn as_flag(&self, value: &str, short: Option<char>, source: &Text) -> Option<Flag> {
        match self.unspanned() {
            Token::Flag(flag) => {
                let name = flag.name().slice(source);

                match flag.kind {
                    FlagKind::Longhand if value == name => Some(*flag),
                    FlagKind::Shorthand => {
                        if let Some(short_hand) = short {
                            if short_hand.to_string() == name {
                                return Some(*flag);
                            }
                        }
                        None
                    }
                    _ => None,
                }
            }
            _ => None,
        }
    }

    pub fn as_pipeline(&self) -> Result<Pipeline, ParseError> {
        match self.unspanned() {
            Token::Pipeline(pipeline) => Ok(pipeline.clone()),
            _ => Err(ParseError::mismatch("pipeline", self.spanned_type_name())),
        }
    }

    pub fn is_whitespace(&self) -> bool {
        match self.unspanned() {
            Token::Whitespace => true,
            _ => false,
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Getters, new)]
#[get = "pub(crate)"]
pub struct DelimitedNode {
    pub(crate) delimiter: Delimiter,
    pub(crate) spans: (Span, Span),
    pub(crate) children: Vec<SpannedToken>,
}

impl HasSpan for DelimitedNode {
    fn span(&self) -> Span {
        self.spans.0.until(self.spans.1)
    }
}

impl PrettyDebugWithSource for DelimitedNode {
    fn pretty_debug(&self, source: &str) -> DebugDocBuilder {
        b::delimit(
            self.delimiter.open(),
            b::intersperse(
                self.children.iter().map(|child| child.pretty_debug(source)),
                b::space(),
            ),
            self.delimiter.close(),
        )
    }
}

impl DelimitedNode {
    pub fn type_name(&self) -> &'static str {
        match self.delimiter {
            Delimiter::Brace => "braced expression",
            Delimiter::Paren => "parenthesized expression",
            Delimiter::Square => "array literal or index operator",
        }
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Delimiter {
    Paren,
    Brace,
    Square,
}

impl Delimiter {
    pub(crate) fn open(self) -> &'static str {
        match self {
            Delimiter::Paren => "(",
            Delimiter::Brace => "{",
            Delimiter::Square => "[",
        }
    }

    pub(crate) fn close(self) -> &'static str {
        match self {
            Delimiter::Paren => ")",
            Delimiter::Brace => "}",
            Delimiter::Square => "]",
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Getters, new)]
#[get = "pub(crate)"]
pub struct PathNode {
    head: Box<SpannedToken>,
    tail: Vec<SpannedToken>,
}

#[cfg(test)]
impl SpannedToken {
    pub fn expect_external(&self) -> Span {
        match self.unspanned() {
            Token::ExternalCommand(span) => *span,
            _ => panic!(
                "Only call expect_external if you checked is_external first, found {:?}",
                self
            ),
        }
    }

    pub fn expect_number(&self) -> RawNumber {
        match self.unspanned() {
            Token::Number(raw_number) => *raw_number,
            other => panic!("Expected number, found {:?}", other),
        }
    }

    pub fn expect_string(&self) -> (Span, Span) {
        match self.unspanned() {
            Token::String(inner_span) => (self.span(), *inner_span),
            other => panic!("Expected string, found {:?}", other),
        }
    }

    pub fn expect_list(&self) -> Spanned<Vec<SpannedToken>> {
        match self.unspanned() {
            Token::Pipeline(pipeline) => pipeline
                .parts()
                .iter()
                .flat_map(|part| part.tokens())
                .cloned()
                .collect::<Vec<SpannedToken>>()
                .spanned(self.span()),
            _ => panic!("Expected list, found {:?}", self),
        }
    }

    pub fn expect_pattern(&self) -> Span {
        match self.unspanned() {
            Token::GlobPattern => self.span(),
            _ => panic!("Expected pattern, found {:?}", self),
        }
    }

    pub fn expect_var(&self) -> (Span, Span) {
        match self.unspanned() {
            Token::Variable(inner_span) => (self.span(), *inner_span),
            Token::ItVariable(inner_span) => (self.span(), *inner_span),
            other => panic!("Expected var, found {:?}", other),
        }
    }

    pub fn expect_dot(&self) -> Span {
        match self.unspanned() {
            Token::EvaluationOperator(EvaluationOperator::Dot) => self.span(),
            other => panic!("Expected dot, found {:?}", other),
        }
    }

    pub fn expect_bare(&self) -> Span {
        match self.unspanned() {
            Token::Bare => self.span(),
            _ => panic!("Expected bare, found {:?}", self),
        }
    }
}
