Documentation
use crate::*;
use crate::parse::{Token, ParseError as E, ParseErrorKind as K};

grammar<'i>;

extern {
    type Location = usize;
    type Error = E;

    enum Token<'i> {
        NEWLINE => Token::Newline,
        NOSP => Token::Join,
        WORD => Token::Word(<&'i str>),
        ESCAPE => Token::Escape(<char>),
        STRING_FRAGMENT => Token::Verbatim(<&'i str>),
        "$VAR" => Token::Variable(<&'i str>),

        "if" => Token::If,
        "else" => Token::Else,
        "for" => Token::For,
        "in" => Token::In,
        "while" => Token::While,
        "break" => Token::Break,
        "continue" => Token::Continue,
        "function" => Token::Function,
        "return" => Token::Return,
        "begin" => Token::Begin,
        "end" => Token::End,
        "switch" => Token::Switch,
        "case" => Token::Case,
        "and" => Token::And,
        "or" => Token::Or,
        "not" => Token::Not,

        "$(" => Token::DollarLParen,
        "(" => Token::LParen,
        ")" => Token::RParen,
        "{" => Token::LBrace,
        "}" => Token::RBrace,
        "," => Token::Comma,
        "|" => Token::Pipe,
        "&|" => Token::AmpPipe,
        ";" => Token::Semi,
        "*" => Token::Star,
        "**" => Token::StarStar,
        "~" => Token::Tilde,
        "~/" => Token::TildeSlash,
        "\"" => Token::DQuote,
        "'" => Token::SQuote,
        "&&" => Token::AmpAmp,
        "||" => Token::PipePipe,

        "&>" => Token::AmpGt,
        ">" => Token::Gt(<&'i str>),
        ">&" => Token::GtAmp(<&'i str>),
        ">?" => Token::GtQus(<&'i str>),
        "<" => Token::Lt(<&'i str>),
        "<&" => Token::LtAmp(<&'i str>),
        "<?" => Token::LtQus(<&'i str>),
        ">>" => Token::GtGt(<&'i str>),
        ">|" => Token::GtPipe(<&'i str>),
    }
}

pub SourceFile: SourceFile =
    <StmtList> => SourceFile { stmts: <> };

StmtList: Vec<Stmt> = {
    => Vec::new(),
    <Stmt> => vec![<>],
    <mut ls:StmtList> EOS <s:Stmt?> => { ls.extend(s); ls },
};

EOS: () = { NEWLINE, ";" };

Stmt: Stmt = {
    #[precedence(level = "0")]
    <Control> => <>,
    #[precedence(level = "0")]
    <pos:@L> <ws:Word+> => Stmt::Command(pos as _, ws),

    #[precedence(level = "1")]
    <pos:@L> <mut s:Stmt> <r:Redirect> => {
        match &mut s {
            Stmt::Redirect(_, _, rs) => { rs.push(r); s },
            _ => Stmt::Redirect(pos as _, s.into(), vec![r]),
        }
    },

    #[precedence(level = "2")]
    #[assoc(side = "right")]
    <lhs:Stmt> <pos:@L> <port:PipeOp> <mut rhs:Stmt> => {
        match &mut rhs {
            Stmt::Pipe(_, pipes, _) => {
                // FIXME: O(n^2) time.
                pipes.insert(0, (lhs, port));
                rhs
            }
            _ => Stmt::Pipe(pos as _, vec![(lhs, port)], rhs.into()),
        }
    },

    #[precedence(level = "3")]
    <pos:@L> "not" <s:Stmt> => Stmt::Not(pos as _, s.into()),

    #[precedence(level = "4")]
    #[assoc(side = "right")]
    <pos:@L> <lhs:Stmt> "and" <rhs:Stmt> => Stmt::BinaryAnd(pos as _, lhs.into(), rhs.into()),
    #[precedence(level = "4")]
    #[assoc(side = "right")]
    <pos:@L> <lhs:Stmt> "&&" <rhs:Stmt> => Stmt::BinaryAnd(pos as _, lhs.into(), rhs.into()),

    #[precedence(level = "5")]
    #[assoc(side = "right")]
    <pos:@L> <lhs:Stmt> "or" <rhs:Stmt> => Stmt::BinaryOr(pos as _, lhs.into(), rhs.into()),
    #[precedence(level = "5")]
    #[assoc(side = "right")]
    <pos:@L> <lhs:Stmt> "||" <rhs:Stmt> => Stmt::BinaryOr(pos as _, lhs.into(), rhs.into()),

    #[precedence(level = "6")]
    <pos:@L> "and" <s:Stmt> => Stmt::UnaryAnd(pos as _, s.into()),
    #[precedence(level = "6")]
    <pos:@L> "or" <s:Stmt> => Stmt::UnaryOr(pos as _, s.into()),
};

PipeOp: RedirectPort = {
    "|" => RedirectPort::STDOUT,
    "&|" => RedirectPort::STDOUT_STDERR,
    <lpos:@L> <port:">|"> =>? {
        port[..port.len() - 2].parse()
        .map_err(|_| E::new(lpos, lpos + port.len() - 2, K::InvalidRedirectPort).into())
    }
}

Redirect: Redirect = {
    <lpos:@L> <tup:RedirectOp> <dest:Word> =>? {
        let (port_str, mode) = tup;
        let port = if port_str == "&" {
            RedirectPort::STDOUT_STDERR
        } else if port_str.is_empty() {
            mode.default_port()
        } else {
            port_str.parse().map_err(|_| E::new(lpos, lpos + port_str.len(), K::InvalidRedirectPort))?
        };
        Ok(Redirect { port, mode, dest })
    },
};

RedirectOp: (&'i str, RedirectMode) = {
    "&>" => ("&", RedirectMode::Write),

    <s:"<" >  => (&s[..s.len() - 1], RedirectMode::Read),
    <s:"<?">  => (&s[..s.len() - 2], RedirectMode::ReadOrNull),
    <s:"<&">  => (&s[..s.len() - 2], RedirectMode::ReadFd),
    <s:">" >  => (&s[..s.len() - 1], RedirectMode::Write),
    <s:">?">  => (&s[..s.len() - 2], RedirectMode::WriteNoClobber),
    <s:">&">  => (&s[..s.len() - 2], RedirectMode::WriteFd),
    <s:">>">  => (&s[..s.len() - 2], RedirectMode::Append),
};

StmtBlock: Stmt =
    <pos:@L> <ls:StmtList> => Stmt::Block(pos as _, ls);

Control: Stmt = {
    <pos:@L> "begin" EOS <ls:StmtList> "end" => Stmt::Block(pos as _, ls),
    <If> => <>,
    <pos:@L> "for" <var:Word> "in" <seq:Word*> EOS <body:StmtBlock> "end" => Stmt::For(pos as _, var, seq, body.into()),
    <pos:@L> "while" <cond:Stmt> EOS <body:StmtBlock> "end" => Stmt::While(pos as _, cond.into(), body.into()),
    <pos:@L> "break" => Stmt::Break(pos as _),
    <pos:@L> "continue" => Stmt::Continue(pos as _),
    <pos:@L> "function" <def:Word*> EOS <body:StmtBlock> "end" => Stmt::Function(pos as _, def, body.into()),
    <pos:@L> "return" <w:Word?> => Stmt::Return(pos as _, w),
    <pos:@L> "switch" <testee:Word> EOS <cases:SwitchCase*> "end" => Stmt::Switch(pos as _, testee, cases),
};

If: Stmt = {
    <pos:@L> "if" <cond:Stmt> EOS <then:StmtBlock> "end" =>
        Stmt::If(pos as _, cond.into(), then.into(), None),
    <pos:@L> "if" <cond:Stmt> EOS <then:StmtBlock> "else" <else_:If> =>
        Stmt::If(pos as _, cond.into(), then.into(), Some(else_.into())),
    <pos:@L> "if" <cond:Stmt> EOS <then:StmtBlock> "else" EOS <else_:StmtBlock> "end" =>
        Stmt::If(pos as _, cond.into(), then.into(), Some(else_.into())),
};

SwitchCase: SwitchCase =
    "case" <globs:Word*> EOS <body:StmtBlock> => SwitchCase { globs, body };

Word: Word = {
    "~" => Word::Complex(vec![WordFrag::Home { slash: false }]),
    <WordJoined>,
};

WordJoined: Word = {
    <SingleWordFrag> => Word::from(<>),
    <String> => Word::Complex(<>),
    <w:WordJoined> NOSP <frag:SingleWordFrag> => w.append(frag),
    <w:WordJoined> NOSP <frags:String> => {
        let fs = match w {
            Word::Simple(w) => std::iter::once(WordFrag::Literal(w)).chain(frags).collect(),
            Word::Complex(mut fs) => { fs.extend(frags); fs },
        };
        Word::Complex(fs)
    }
};

SingleWordFrag: WordFrag = {
    "~/" => WordFrag::Home { slash: true },
    <WORD> => WordFrag::Literal(<>.into()),
    <lpos:@L> <s:"$VAR"> <rpos:@R> =>? {
        let name = s.trim_start_matches('$');
        let deref = u8::try_from(s.len() - name.len() - 1).map_err(|_| E::new(lpos, rpos, K::TooManyDeref))?;
        Ok(WordFrag::Variable { name: name.into(), deref })
    },
    <ESCAPE> => WordFrag::Literal(<>.into()),
    "*" => WordFrag::Wildcard,
    "**" => WordFrag::WildcardRecursive,
    "$(" <StmtBlock> ")" => WordFrag::Command(<>),
    "(" <StmtBlock> ")" => WordFrag::Command(<>),
    <BraceWord>,
};

String: Vec<WordFrag> = {
    "\"" <DQuoteFrag*> "\"" => <>,
    "'" <SQuoteFrag*> "'" => <>,
};
DQuoteFrag: WordFrag = {
    <lpos:@L> <s:"$VAR"> <rpos:@R> =>? {
        let name = s.trim_start_matches('$');
        let deref = u8::try_from(s.len() - name.len() - 1).map_err(|_| E::new(lpos, rpos, K::TooManyDeref))?;
        Ok(WordFrag::VariableNoSplit { name: name.into(), deref })
    },
    "$(" <StmtBlock> ")" => WordFrag::CommandNoSplit(<>),
    <STRING_FRAGMENT> => WordFrag::Literal(<>.into()),
};
SQuoteFrag: WordFrag =
    <STRING_FRAGMENT> => WordFrag::Literal(<>.into());

BraceWord: WordFrag =
    "{" <mut alts:(<BraceAltWord> ",")*> <last:BraceAltWord> "}" => {
        alts.push(last);
        WordFrag::Brace(alts)
    };

BraceAltWord: Word = <BraceAlt> => Word::Complex(<>);

BraceAlt: Vec<WordFrag> = {
    => Vec::new(),
    <mut frags:BraceAlt> <s:STRING_FRAGMENT> => { frags.push(WordFrag::Literal(s.into())); frags },
    // NOSP is not emitted inside braces.
    <mut frags:BraceAlt> <w:SingleWordFrag> => { frags.push(w); frags },
    <mut frags:BraceAlt> <s:String> => { frags.extend(s); frags },
};