use std::fmt;

use nom::{
    branch::alt,
    bytes::{
        complete::{self, tag},
        streaming,
    },
    character::complete::{anychar, digit0, digit1, hex_digit1, oct_digit1},
    combinator::{map, recognize, value, verify},
    multi::{many0, many1},
    sequence::{delimited, preceded, terminated, tuple},
    IResult,
};

#[derive(Debug, PartialEq)]
pub enum WordSegment<'a> {
    Literal(&'a str),
    Var(&'a str),
    SingleQuote(&'a str),
    DoubleQuote(Vec<DoubleQuotedSegment<'a>>),
    Escape(Escape<'a>),
}

#[derive(Debug, PartialEq)]
pub enum DoubleQuotedSegment<'a> {
    Literal(&'a str),
    Var(&'a str),
    Escape(Escape<'a>),
}

#[derive(Debug, PartialEq)]
pub struct WordToken<'a>(pub Vec<WordSegment<'a>>);

#[derive(Debug, PartialEq, Clone)]
pub enum Escape<'a> {
    Nul,
    Letter(char),
    Octal(&'a str),
    Hex(&'a str),
}

#[derive(Debug, PartialEq)]
pub enum Keyword {
    If,
    Then,
    Else,
    Fi,
    For,
    While,
    Do,
    Done,
    Case,
    Esac,
    In,
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Punct {
    Lt,
    Gt,
    Ltlt,
    Gtgt,
    NotEqual,
    DoubleSemicolon,
    Ampersand,
    And,
    Pipe,
    Or,
    Equal,
    LBrace,
    RBrace,
    LBracket,
    RBracket,
    LParen,
    RParen,
    Not,
    Semicolon,
    Newline,
}

#[derive(Debug, PartialEq)]
pub enum Token<'a> {
    Word(WordToken<'a>),
    Keyword(Keyword),
    Punct(Punct),
    Redirect(Redirect<'a>),
}

#[derive(Debug, PartialEq)]
pub enum OutputRedirectTarget<'a> {
    Filename(WordToken<'a>),
    Descriptor(&'a str),
}

#[derive(Debug, PartialEq)]
pub struct OutputRedirect<'a> {
    pub source: Option<&'a str>,
    pub target: OutputRedirectTarget<'a>,
    pub append: bool,
}

#[derive(Debug, PartialEq)]
pub enum InputRedirect<'a> {
    Filename(WordToken<'a>),
    Heredoc(&'a str),
}

// TODO heredoc
#[derive(Debug, PartialEq)]
pub enum Redirect<'a> {
    // <xxx
    Input(InputRedirect<'a>),
    // 123>[>][&]...
    Output(OutputRedirect<'a>),
}

fn lex_escape(i: &str) -> IResult<&str, Escape> {
    preceded(
        tag("\\"),
        alt((
            map(preceded(tag("0"), oct_digit1), Escape::Octal),
            map(preceded(tag("x"), hex_digit1), Escape::Hex),
            map(preceded(tag("X"), hex_digit1), Escape::Hex),
            value(Escape::Nul, tag("0")),
            map(anychar, Escape::Letter),
        )),
    )(i)
}

fn lex_literal_word_segment(i: &str) -> IResult<&str, &str> {
    // Anything that is not a \, $, ', ", `, SPACE, TAB or NEWLINE
    const EXCLUDE: &str = "\\$'\"` \t\n<>[]{}!&;";
    verify(streaming::is_not(EXCLUDE), |s: &str| !s.is_empty())(i)
}

fn lex_single_quote_word_segment(i: &str) -> IResult<&str, &str> {
    delimited(tag("'"), recognize(many0(streaming::is_not("'"))), tag("'"))(i)
}

fn lex_identifier(i: &str) -> IResult<&str, &str> {
    // Can be anything besides &, ', `, ", ;, :, \, SPACE, TAB or NEWLINE
    const EXCLUDE0: &str = "\\&'\"`; \t\n<>[]{}()";
    recognize(streaming::is_not(EXCLUDE0))(i)
}

fn lex_braced_var_segment(i: &str) -> IResult<&str, &str> {
    delimited(tag("{"), lex_identifier, tag("}"))(i)
}

fn lex_var_segment(i: &str) -> IResult<&str, &str> {
    preceded(tag("$"), alt((lex_braced_var_segment, lex_identifier)))(i)
}

fn lex_double_quoted_literal_segment(i: &str) -> IResult<&str, &str> {
    const EXCLUDE: &str = "\\$`\"";
    recognize(streaming::is_not(EXCLUDE))(i)
}

fn lex_double_quoted_segment(i: &str) -> IResult<&str, DoubleQuotedSegment> {
    alt((
        map(lex_escape, DoubleQuotedSegment::Escape),
        map(lex_var_segment, DoubleQuotedSegment::Var),
        map(
            lex_double_quoted_literal_segment,
            DoubleQuotedSegment::Literal,
        ),
    ))(i)
}

fn lex_double_quote_word_segment(i: &str) -> IResult<&str, Vec<DoubleQuotedSegment>> {
    delimited(tag("\""), many0(lex_double_quoted_segment), tag("\""))(i)
}

fn lex_word_segment(i: &str) -> IResult<&str, WordSegment> {
    alt((
        map(lex_escape, WordSegment::Escape),
        map(lex_var_segment, WordSegment::Var),
        map(lex_double_quote_word_segment, WordSegment::DoubleQuote),
        map(lex_single_quote_word_segment, WordSegment::SingleQuote),
        map(lex_literal_word_segment, WordSegment::Literal),
    ))(i)
}

fn lex_word(i: &str) -> IResult<&str, WordToken> {
    map(terminated(many1(lex_word_segment), whitespace0), WordToken)(i)
}

fn lex_word_or_keyword(i: &str) -> IResult<&str, Token> {
    map(lex_word, |word| {
        if let Some(kw) = word.as_keyword() {
            Token::Keyword(kw)
        } else {
            Token::Word(word)
        }
    })(i)
}

fn lex_punct(i: &str) -> IResult<&str, Punct> {
    alt((
        value(Punct::Ltlt, tag("<<")),
        value(Punct::Gtgt, tag(">>")),
        value(Punct::And, tag("&&")),
        value(Punct::Or, tag("||")),
        value(Punct::DoubleSemicolon, tag(";;")),
        value(Punct::NotEqual, tag("!=")),
        value(Punct::Lt, tag("<")),
        value(Punct::Gt, tag(">")),
        value(Punct::Pipe, tag("|")),
        value(Punct::Ampersand, tag("&")),
        value(Punct::LParen, tag("(")),
        value(Punct::RParen, tag(")")),
        value(Punct::LBrace, tag("{")),
        value(Punct::RBrace, tag("}")),
        value(Punct::LBracket, tag("[")),
        value(Punct::RBracket, tag("]")),
        value(Punct::Equal, tag("=")),
        value(Punct::Not, tag("!")),
        value(Punct::Semicolon, tag(";")),
        value(Punct::Newline, tag("\n")),
    ))(i)
}

fn lex_input_redirect(i: &str) -> IResult<&str, InputRedirect> {
    alt((
        map(preceded(tag("<<"), lex_identifier), InputRedirect::Heredoc),
        map(preceded(tag("<"), lex_word), InputRedirect::Filename),
    ))(i)
}

fn lex_output_redirect_target(i: &str) -> IResult<&str, OutputRedirectTarget> {
    alt((
        map(preceded(tag("&"), digit1), OutputRedirectTarget::Descriptor),
        map(lex_word, OutputRedirectTarget::Filename),
    ))(i)
}

fn lex_output_redirect(i: &str) -> IResult<&str, OutputRedirect> {
    map(
        tuple((
            digit0,
            alt((tag(">>"), tag(">"))),
            lex_output_redirect_target,
        )),
        |(source, token, target)| {
            let source = (!source.is_empty()).then_some(source);
            let append = token == ">>";
            OutputRedirect {
                source,
                target,
                append,
            }
        },
    )(i)
}

fn lex_redirect(i: &str) -> IResult<&str, Redirect> {
    alt((
        map(lex_output_redirect, Redirect::Output),
        map(lex_input_redirect, Redirect::Input),
    ))(i)
}

fn whitespace0(i: &str) -> IResult<&str, &str> {
    recognize(many0(complete::is_a(" \t")))(i)
}

pub fn lex_token(i: &str) -> IResult<&str, Token> {
    preceded(
        whitespace0,
        alt((
            map(lex_redirect, Token::Redirect),
            map(lex_punct, Token::Punct),
            lex_word_or_keyword,
        )),
    )(i)
}

pub fn lex_tokens(mut i: &str) -> IResult<&str, Vec<Token>> {
    let mut tokens = vec![];
    while !i.is_empty() {
        let (rest, token) = lex_token(i)?;
        i = rest;
        tokens.push(token);
    }
    Ok((i, tokens))
}

impl WordToken<'_> {
    pub fn as_keyword(&self) -> Option<Keyword> {
        if self.0.len() == 1 {
            let seg0 = &self.0[0];
            match seg0 {
                WordSegment::Literal("if") => Some(Keyword::If),
                WordSegment::Literal("then") => Some(Keyword::Then),
                WordSegment::Literal("else") => Some(Keyword::Else),
                WordSegment::Literal("fi") => Some(Keyword::Fi),
                WordSegment::Literal("case") => Some(Keyword::Case),
                WordSegment::Literal("esac") => Some(Keyword::Esac),
                WordSegment::Literal("for") => Some(Keyword::For),
                WordSegment::Literal("while") => Some(Keyword::While),
                WordSegment::Literal("do") => Some(Keyword::Do),
                WordSegment::Literal("done") => Some(Keyword::Done),
                WordSegment::Literal("in") => Some(Keyword::In),
                _ => None,
            }
        } else {
            None
        }
    }

    pub fn ends_with(&self, ch: char) -> bool {
        match self.0.last() {
            Some(WordSegment::Literal(lit)) => lit.ends_with(ch),
            _ => false,
        }
    }
}

impl Keyword {
    pub fn to_word(&self) -> WordToken<'static> {
        let name = match self {
            Self::If => "if",
            Self::Then => "then",
            Self::Else => "else",
            Self::Fi => "fi",
            Self::For => "for",
            Self::While => "while",
            Self::Do => "do",
            Self::Done => "done",
            Self::Case => "case",
            Self::Esac => "esac",
            Self::In => "in",
        };
        WordToken(vec![WordSegment::Literal(name)])
    }
}

impl Escape<'_> {
    pub fn to_char(&self) -> Option<char> {
        match self {
            Self::Nul => Some('\0'),
            &Self::Hex(x)
                if let Some(ch) = u32::from_str_radix(x, 16).ok().and_then(char::from_u32) =>
            {
                Some(ch)
            }
            &Self::Octal(x)
                if let Some(ch) = u32::from_str_radix(x, 8).ok().and_then(char::from_u32) =>
            {
                Some(ch)
            }
            &Self::Letter('n') => Some('\n'),
            &Self::Letter('t') => Some('\t'),
            &Self::Letter('r') => Some('\r'),
            &Self::Letter(' ') => Some(' '),
            _ => None,
        }
    }
}

impl fmt::Display for WordToken<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for segment in self.0.iter() {
            write!(f, "{segment}")?;
        }
        Ok(())
    }
}

impl fmt::Display for DoubleQuotedSegment<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Literal(text) => write!(f, "{}", text.escape_default()),
            Self::Escape(esc) => write!(f, "{esc}"),
            Self::Var(var) => write!(f, "${{{var}}}"),
        }
    }
}

impl fmt::Display for Escape<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "\\")?;
        match self {
            Self::Nul => write!(f, "0"),
            Self::Hex(val) => write!(f, "x{val}"),
            Self::Octal(val) => write!(f, "0{val}"),
            Self::Letter(l) => write!(f, "{l}"),
        }
    }
}

impl fmt::Display for WordSegment<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Var(var) => write!(f, "${{{var}}}"),
            Self::Literal(text) => write!(f, "{text}"),
            Self::Escape(escape) => write!(f, "{escape}"),
            Self::SingleQuote(quote) => write!(f, "'{quote}'"),
            Self::DoubleQuote(quote) => {
                write!(f, "\"")?;
                for seg in quote {
                    write!(f, "{seg}")?;
                }
                write!(f, "\"")
            }
        }
    }
}

impl fmt::Display for Keyword {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.to_word(), f)
    }
}

impl fmt::Display for Punct {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let t = match self {
            Self::Lt => "<",
            Self::Gt => ">",
            Self::Ltlt => "<<",
            Self::Gtgt => ">>",
            Self::LBrace => "{",
            Self::RBrace => "}",
            Self::LParen => "(",
            Self::RParen => ")",
            Self::LBracket => "[",
            Self::RBracket => "]",
            Self::Not => "!",
            Self::Equal => "=",
            Self::NotEqual => "!=",
            Self::Newline => "\\n",
            Self::Ampersand => "&",
            Self::And => "&&",
            Self::Or => "||",
            Self::Pipe => "|",
            Self::Semicolon => ";",
            Self::DoubleSemicolon => ";;",
        };
        write!(f, "{t}")
    }
}

impl fmt::Display for InputRedirect<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Filename(filename) => write!(f, "<{filename}"),
            Self::Heredoc(terminator) => write!(f, "<<{terminator}"),
        }
    }
}

impl fmt::Display for OutputRedirectTarget<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Filename(filename) => write!(f, "{filename}"),
            Self::Descriptor(descriptor) => write!(f, "&{descriptor}"),
        }
    }
}

impl fmt::Display for OutputRedirect<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(source) = self.source {
            write!(f, "{source}")?;
        }
        let prefix = if self.append { ">>" } else { ">" };
        write!(f, "{prefix}{}", self.target)
    }
}

impl fmt::Display for Redirect<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Input(input) => write!(f, "{input}"),
            Self::Output(output) => write!(f, "{output}"),
        }
    }
}

impl fmt::Display for Token<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Word(word) => fmt::Display::fmt(word, f),
            Self::Punct(punct) => fmt::Display::fmt(punct, f),
            Self::Keyword(kw) => fmt::Display::fmt(kw, f),
            Self::Redirect(redirect) => fmt::Display::fmt(redirect, f),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::syntax::lex::{
        lex_double_quote_word_segment, lex_literal_word_segment, lex_single_quote_word_segment,
        lex_token, lex_word, DoubleQuotedSegment, Escape, Keyword, OutputRedirect,
        OutputRedirectTarget, Punct, Redirect, Token, WordSegment, WordToken,
    };

    use super::lex_tokens;

    #[test]
    fn test_lex_complete() {
        let x = r#"if a; then
echo b
echo c
fi
"#;
        let (r, o) = lex_tokens(x).unwrap();
        assert_eq!(r, "");
        assert_eq!(
            o,
            vec![
                Token::Keyword(Keyword::If),
                Token::Word(WordToken(vec![WordSegment::Literal("a")])),
                Token::Punct(Punct::Semicolon),
                Token::Keyword(Keyword::Then),
                Token::Punct(Punct::Newline),
                Token::Word(WordToken(vec![WordSegment::Literal("echo")])),
                Token::Word(WordToken(vec![WordSegment::Literal("b")])),
                Token::Punct(Punct::Newline),
                Token::Word(WordToken(vec![WordSegment::Literal("echo")])),
                Token::Word(WordToken(vec![WordSegment::Literal("c")])),
                Token::Punct(Punct::Newline),
                Token::Keyword(Keyword::Fi),
                Token::Punct(Punct::Newline),
            ]
        );
    }

    #[test]
    fn test_lex_literal_word_segment() {
        let x = "a b c";
        assert_eq!(lex_literal_word_segment(x).unwrap().1, "a");
        let x = "a$b b c";
        assert_eq!(lex_literal_word_segment(x).unwrap().1, "a");
        let x = r#"a"b" b c"#;
        assert_eq!(lex_literal_word_segment(x).unwrap().1, "a");
        let x = "a1#.,/@`xyz`w";
        assert_eq!(lex_literal_word_segment(x).unwrap().1, "a1#.,/@");
        let x = r#"a\ b"#;
        assert_eq!(lex_literal_word_segment(x).unwrap().1, "a");
    }

    #[test]
    fn test_lex_single_quote_word_segment() {
        let x = r"'a'";
        assert_eq!(lex_single_quote_word_segment(x).unwrap().1, "a");
        let x = r"'\''";
        assert_eq!(lex_single_quote_word_segment(x).unwrap(), ("'", r"\"));
        let x = "''''";
        assert_eq!(lex_single_quote_word_segment(x).unwrap(), ("''", ""));
    }

    #[test]
    fn test_lex_double_quote_word_segment() {
        let x = r#""a\$ b \"$c:x\ $d\033""#;
        assert_eq!(
            lex_double_quote_word_segment(x).unwrap().1,
            vec![
                DoubleQuotedSegment::Literal("a"),
                DoubleQuotedSegment::Escape(Escape::Letter('$')),
                DoubleQuotedSegment::Literal(" b "),
                DoubleQuotedSegment::Escape(Escape::Letter('"')),
                DoubleQuotedSegment::Var("c:x"),
                DoubleQuotedSegment::Escape(Escape::Letter(' ')),
                DoubleQuotedSegment::Var("d"),
                DoubleQuotedSegment::Escape(Escape::Octal("33")),
            ]
        );
    }

    #[test]
    fn test_lex_word() {
        let x = r#"a1_/-.'q $a\'"q $?\n\"2"c\ d e"#;
        assert_eq!(
            lex_word(x).unwrap().1,
            WordToken(vec![
                WordSegment::Literal("a1_/-."),
                WordSegment::SingleQuote("q $a\\"),
                WordSegment::DoubleQuote(vec![
                    DoubleQuotedSegment::Literal("q "),
                    DoubleQuotedSegment::Var("?"),
                    DoubleQuotedSegment::Escape(Escape::Letter('n')),
                    DoubleQuotedSegment::Escape(Escape::Letter('"')),
                    DoubleQuotedSegment::Literal("2"),
                ]),
                WordSegment::Literal("c"),
                WordSegment::Escape(Escape::Letter(' ')),
                WordSegment::Literal("d"),
            ])
        );
    }

    #[test]
    fn test_lex_token() {
        let x = r#"if condition; then ..."#;
        assert_eq!(lex_token(x).unwrap().1, Token::Keyword(Keyword::If));
        let x = r#"if'' condition; then ..."#;
        assert_eq!(
            lex_token(x).unwrap().1,
            Token::Word(WordToken(vec![
                WordSegment::Literal("if"),
                WordSegment::SingleQuote("")
            ]))
        );
        let x = "1>>a\n";
        assert_eq!(
            lex_token(x).unwrap().1,
            Token::Redirect(Redirect::Output(OutputRedirect {
                source: Some("1"),
                target: OutputRedirectTarget::Filename(WordToken(vec![WordSegment::Literal("a")])),
                append: true
            }))
        );
        let x = ">&2\n";
        assert_eq!(
            lex_token(x).unwrap().1,
            Token::Redirect(Redirect::Output(OutputRedirect {
                source: None,
                target: OutputRedirectTarget::Descriptor("2"),
                append: false
            }))
        );
    }
}
