use crate::frontend::ast::*;
use crate::frontend::parser::{Cst, NodeRef};
use crate::frontend::sema::*;
use crate::VERSION;
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::Path;

trait Indent {
    fn indent(&self, level: usize) -> String;
}

impl Indent for &str {
    fn indent(&self, level: usize) -> String {
        let mut result = String::new();
        for l in self.lines() {
            if l.is_empty() {
                continue;
            }
            result.push_str(&"    ".repeat(level));
            result.push_str(l);
            result.push('\n');
        }
        if !self.ends_with('\n') {
            result.pop();
        }
        result
    }
}

impl Indent for String {
    fn indent(&self, level: usize) -> String {
        self.as_str().indent(level)
    }
}

trait Generator {
    fn pattern(&self, level: usize) -> String;
    fn error(&self, level: usize, token_symbols: &HashMap<&str, &str>) -> String;
}

impl<'a> Generator for std::collections::BTreeSet<TokenName<'a>> {
    fn pattern(&self, level: usize) -> String {
        let symbols: Vec<_> = self.iter().map(|s| format!("Token::{}", s.0)).collect();
        symbols.join(&format!("\n{}| ", "    ".repeat(level)))
    }
    fn error(&self, level: usize, token_symbols: &HashMap<&str, &str>) -> String {
        if !self.is_empty() {
            let symbols: Vec<_> = self
                .iter()
                .map(|s| format!("\"{}\"", token_symbols[s.0]))
                .collect();
            symbols.join(&format!(",\n{}", "    ".repeat(level)))
        } else {
            "EOF".to_string()
        }
    }
}

pub struct RustOutput {}

impl RustOutput {
    pub fn run(cst: &Cst, sema: &SemanticData, input: &Path, output: &Path) -> std::io::Result<()> {
        let file = File::cast(cst, NodeRef::ROOT).unwrap();
        let mut generated_file = std::fs::File::create(output.join("generated.rs"))?;
        generated_file.write_all(format!("// generated by lelwel {}\n\n", VERSION).as_bytes())?;
        Self::output_generated(cst, sema, file, &mut generated_file)?;

        let parser_path = input.parent().unwrap().join("parser.rs");
        if !parser_path.exists() {
            Self::output_parser(cst, file, sema, &parser_path)?;
        }

        Ok(())
    }

    fn output_parser(
        cst: &Cst,
        file: File,
        sema: &SemanticData,
        path: &Path,
    ) -> std::io::Result<()> {
        let mut parser_file = std::fs::File::create(path)?;

        let mut token_enumerators = "{\n    EOF,\n".to_string();
        for token in file.token_decls(cst) {
            if let Some((symbol, _)) = token.symbol(cst) {
                if !(symbol.is_empty()
                    || symbol.starts_with("'<") && symbol.ends_with(">'") && symbol.len() > 4)
                {
                    token_enumerators += "    #[token(\"";
                    token_enumerators += &symbol[1..symbol.len() - 1];
                    token_enumerators += "\")]\n";
                }
            }
            let (name, _) = token.name(cst).unwrap();
            token_enumerators += "    ";
            token_enumerators += name;
            token_enumerators += ",\n";
        }

        let template =
            include_str!("../skeleton/parser.rs").replace("{\n    EOF,\n", &token_enumerators);

        parser_file.write_all(template.as_bytes())?;

        Self::output_predicates_and_actions(&mut parser_file, sema, false)
    }

    fn output_predicates_and_actions(
        output: &mut std::fs::File,
        sema: &SemanticData,
        is_trait: bool,
    ) -> std::io::Result<()> {
        output.write_all(if is_trait {
            b"trait PredicatesAndActions {\
            \n    /// Called when a new syntax tree node is created\
            \n    #[allow(clippy::ptr_arg)]\
            \n    fn build(&mut self, _rule: Rule, _node: NodeRef, _diags: &mut Vec<Diagnostic>) {}\n"
        } else {
            b"impl<'a> PredicatesAndActions for Parser<'a> {\n"
        })?;
        let mut predicates = HashSet::new();
        for (rule, num) in sema.predicates.values() {
            if predicates.contains(&(rule, num)) {
                continue;
            }
            predicates.insert((rule, num));
            output.write_all(
                format!(
                    "    fn predicate_{rule}_{num}(&self) -> bool{}\n",
                    if is_trait {
                        ";"
                    } else {
                        " {\n        todo!()\n    }"
                    }
                )
                .as_bytes(),
            )?;
        }
        let mut actions = HashSet::new();
        for (rule, num) in sema.actions.values() {
            if actions.contains(&(rule, num)) {
                continue;
            }
            actions.insert((rule, num));
            output.write_all(
                format!(
                    "    fn action_{rule}_{num}(&mut self, diags: &mut Vec<Diagnostic>){}\n",
                    if is_trait {
                        ";"
                    } else {
                        " {\n        todo!()\n    }"
                    }
                )
                .as_bytes(),
            )?;
        }
        output.write_all(b"}\n")
    }

    fn output_node_kind_decl(
        output: &mut std::fs::File,
        has_rule_binding: bool,
        name: &str,
        level: usize,
        is_decl: bool,
    ) -> std::io::Result<()> {
        if has_rule_binding {
            output.write_all(
                format!(
                    "{}node_kind = Rule::{};\n",
                    if is_decl { "let mut " } else { "" },
                    Self::snake_to_pascal_case(name),
                )
                .indent(level)
                .as_bytes(),
            )?;
        }
        Ok(())
    }

    fn output_cst_close(
        output: &mut std::fs::File,
        has_rule_binding: bool,
        name: &str,
        level: usize,
        assign_lhs: bool,
    ) -> std::io::Result<()> {
        let lhs = if assign_lhs { "lhs = " } else { "" };
        if has_rule_binding {
            output.write_all(
                format!("{lhs}self.close(m, node_kind, diags);\n")
                    .indent(level)
                    .as_bytes(),
            )
        } else {
            output.write_all(
                format!(
                    "{lhs}self.close(m, Rule::{}, diags);\n",
                    Self::snake_to_pascal_case(name),
                )
                .indent(level)
                .as_bytes(),
            )
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn output_normal_rule(
        cst: &Cst,
        sema: &SemanticData,
        output: &mut std::fs::File,
        token_symbols: &HashMap<&str, &str>,
        has_rule_binding: bool,
        name: &str,
        regex: Regex,
        is_start: bool,
    ) -> std::io::Result<()> {
        output.write_all(b"        let m = self.cst.open();\n")?;
        Self::output_node_kind_decl(output, has_rule_binding, name, 2, true)?;
        if is_start {
            output.write_all(b"        self.init_skip();\n")?;
        }
        Self::output_regex(cst, sema, regex, output, 2, token_symbols, false, name)?;
        if is_start {
            output.write_all(
                b"        if self.current != Token::EOF {\
                \n            self.error(diags, err![self.span(), \"<end of file>\"]);\
                \n            let error_tree = self.cst.open();\
                \n            loop {\
                \n                match self.cst.tokens.get(self.pos) {\
                \n                    None => break,\
                \n                    _ => self.cst.advance(),\
                \n                }\
                \n                self.pos += 1;\
                \n            }\
                \n            self.close(error_tree, Rule::Error, diags);\
                \n        }\n",
            )?;
        }
        Self::output_cst_close(output, has_rule_binding, name, 2, false)
    }

    #[allow(clippy::too_many_arguments)]
    fn output_left_recursive_rule(
        cst: &Cst,
        sema: &SemanticData,
        output: &mut std::fs::File,
        token_symbols: &HashMap<&str, &str>,
        has_rule_binding: bool,
        name: &str,
        regex: Regex,
        branches: &[Regex],
    ) -> std::io::Result<()> {
        let ops = if let Regex::Alternation(alt) = regex {
            alt.operands(cst)
        } else {
            unreachable!();
        };
        Self::output_node_kind_decl(output, has_rule_binding, name, 2, true)?;
        output.write_all(
            b"        let mut lhs = self.cst.mark();\
            \n        match self.current {\n",
        )?;
        for op in ops {
            if branches.contains(&op) {
                continue;
            }
            output.write_all(
                format!(
                    "{}{} => {{\n",
                    sema.predict_sets[&op.syntax()].pattern(0),
                    Self::get_predicate(cst, name, op)
                )
                .indent(3)
                .as_bytes(),
            )?;
            let is_forwarding = if let Regex::Name(name) = op {
                sema.decl_bindings
                    .get(&name.syntax())
                    .map_or(false, |n| RuleDecl::cast(cst, *n).is_some())
            } else {
                false
            };
            if is_forwarding {
                Self::output_regex(cst, sema, op, output, 5, token_symbols, false, name)?;
            } else {
                output.write_all(b"                    let m = self.cst.open();\n")?;
                Self::output_regex(cst, sema, op, output, 5, token_symbols, false, name)?;
                Self::output_cst_close(output, has_rule_binding, name, 5, false)?;
            }
            output.write_all("}\n".indent(3).as_bytes())?;
        }
        output.write_all(
            format!(
                "    _ => {{\
               \n        self.error(diags, err![self.span(), {}]);\
               \n    }}\
               \n}}\n",
                sema.predict_sets[&regex.syntax()].error(5, token_symbols)
            )
            .indent(2)
            .as_bytes(),
        )?;
        output.write_all(b"        loop {\n")?;
        Self::output_node_kind_decl(output, has_rule_binding, name, 3, false)?;
        output.write_all(b"            match self.current {\n")?;
        for branch in branches {
            let ops = if let Regex::Concat(concat) = branch {
                concat.operands(cst)
            } else {
                unreachable!()
            };
            for (i, op) in ops
                .filter(|op| !matches!(op, Regex::Predicate(_)))
                .skip(1)
                .enumerate()
            {
                if i == 0 {
                    output.write_all(
                        format!(
                            "{}{} => {{\n",
                            sema.predict_sets[&op.syntax()].pattern(0),
                            Self::get_predicate(cst, name, *branch)
                        )
                        .indent(4)
                        .as_bytes(),
                    )?;
                    output
                        .write_all("let m = self.cst.open_before(lhs);\n".indent(5).as_bytes())?;
                }
                Self::output_regex(cst, sema, op, output, 5, token_symbols, false, name)?;
            }
            Self::output_cst_close(output, has_rule_binding, name, 5, true)?;
            output.write_all(b"                }\n")?;
        }
        output.write_all(
            "    _ => {\
           \n        break;\
           \n    }\
           \n}\n"
                .indent(3)
                .as_bytes(),
        )?;
        output.write_all("}\n".indent(2).as_bytes())
    }

    fn output_operator_precedence_rule(
        cst: &Cst,
        sema: &SemanticData,
        output: &mut std::fs::File,
        name: &str,
        regex: Regex,
        branches: &[Regex],
    ) -> std::io::Result<()> {
        let exit = if let Regex::Alternation(alt) = regex {
            alt.operands(cst).find(|op| !branches.contains(op)).unwrap()
        } else {
            unreachable!();
        };
        let exit_name = if let Regex::Name(name) = exit {
            name.value(cst).unwrap().0
        } else {
            unreachable!()
        };
        output.write_all(b"        let lhs = self.cst.mark();\n")?;
        output.write_all(
            b"        fn rec(\
            \n            parser: &mut Parser,\
            \n            diags: &mut Vec<Diagnostic>,\
            \n            min_prec: usize,\
            \n            mut lhs: MarkClosed,\
            \n        ) {\
            \n            loop {\
            \n                let prec = match parser.current {\n",
        )?;
        for (i, op) in branches.iter().rev().enumerate() {
            let mut ops = if let Regex::Concat(concat) = op {
                concat.operands(cst)
            } else {
                unreachable!()
            };
            let op = ops.nth(1).unwrap();
            output.write_all(
                format!("{} => {},\n", sema.predict_sets[&op.syntax()].pattern(0), i)
                    .indent(5)
                    .as_bytes(),
            )?;
        }
        output.write_all(
            format!(
                "                      _ => return,\
               \n                }};\
               \n                if prec < min_prec {{\
               \n                    return;\
               \n                }}\
               \n                parser.advance(false);\
               \n                let m = parser.cst.open_before(lhs);\
               \n                let rhs = parser.cst.mark();\
               \n                parser.{exit_name}(diags);\
               \n                loop {{\
               \n                    let (next_prec, left_assoc) = match parser.current {{\n"
            )
            .as_bytes(),
        )?;
        for (i, op) in branches.iter().rev().enumerate() {
            let mut ops = if let Regex::Concat(concat) = op {
                concat.operands(cst)
            } else {
                unreachable!()
            };
            let op = ops.nth(1).unwrap();
            for operator in sema.predict_sets[&op.syntax()].iter() {
                output.write_all(
                    format!(
                        "Token::{} => ({i}, {}),\n",
                        operator.0,
                        !sema.right_associative.contains(operator.0),
                    )
                    .indent(6)
                    .as_bytes(),
                )?;
            }
        }
        output.write_all(
            format!(
                "                        _ => break,\
               \n                    }};\
               \n                    if !(prec < next_prec || (!left_assoc && prec == next_prec)) {{\
               \n                        break;\
               \n                    }}\
               \n                    rec(\
               \n                        parser,\
               \n                        diags,\
               \n                        prec + if next_prec > prec {{ 1 }} else {{ 0 }},\
               \n                        rhs,\
               \n                    );\
               \n                }}\
               \n                lhs = parser.close(m, Rule::{}, diags);\
               \n            }}\
               \n        }}\
               \n        self.{exit_name}(diags);\
               \n        rec(self, diags, 0, lhs);\n",
               Self::snake_to_pascal_case(name)
        ).as_bytes())
    }

    #[allow(clippy::too_many_arguments)]
    fn output_right_recursice_forwarding_rule(
        cst: &Cst,
        sema: &SemanticData,
        output: &mut std::fs::File,
        token_symbols: &HashMap<&str, &str>,
        has_rule_binding: bool,
        name: &str,
        regex: Regex,
        branches: &[Regex],
    ) -> std::io::Result<()> {
        output.write_all(b"        let lhs = self.cst.mark();\n")?;
        Self::output_node_kind_decl(output, has_rule_binding, name, 2, true)?;
        if let Regex::Alternation(alt) = regex {
            output.write_all("match self.current {\n".indent(2).as_bytes())?;
            for op in alt.operands(cst) {
                let is_exit = branches.contains(&op);
                output.write_all(
                    format!(
                        "{}{} => {{\n",
                        sema.predict_sets[&op.syntax()].pattern(0),
                        Self::get_predicate(cst, name, op)
                    )
                    .indent(3)
                    .as_bytes(),
                )?;
                if !is_exit {
                    output.write_all(b"                let m = self.cst.open_before(lhs);\n")?;
                }
                Self::output_regex(cst, sema, op, output, 4, token_symbols, false, name)?;
                if !is_exit {
                    Self::output_cst_close(output, has_rule_binding, name, 4, false)?;
                }
                output.write_all("}\n".indent(3).as_bytes())?;
            }
            output.write_all(
                format!(
                    "    _ => {{\
                   \n        self.error(diags, err![self.span(), {}]);\
                   \n    }}\
                   \n}}\n",
                    sema.predict_sets[&regex.syntax()].error(5, token_symbols),
                )
                .indent(2)
                .as_bytes(),
            )?;
        }
        Ok(())
    }

    fn output_conditional_forwarding_rule(
        cst: &Cst,
        sema: &SemanticData,
        output: &mut std::fs::File,
        token_symbols: &HashMap<&str, &str>,
        has_rule_binding: bool,
        name: &str,
        regex: Regex,
    ) -> std::io::Result<()> {
        let (head, tail) = if let Regex::Concat(concat) = regex {
            let mut ops = concat.operands(cst);
            (ops.next().unwrap(), ops)
        } else {
            unreachable!();
        };
        output.write_all(
            b"        let lhs = self.cst.mark();\
            \n        let mut m = None;\n",
        )?;
        Self::output_node_kind_decl(output, has_rule_binding, name, 2, true)?;
        Self::output_regex(cst, sema, head, output, 2, token_symbols, false, name)?;
        for op in tail {
            Self::output_regex(cst, sema, op, output, 2, token_symbols, true, name)?;
        }
        output.write_all(b"        if let Some(m) = m {\n")?;
        Self::output_cst_close(output, has_rule_binding, name, 3, false)?;
        output.write_all(b"        }\n")
    }

    fn output_maybe_empty_rule(
        cst: &Cst,
        sema: &SemanticData,
        output: &mut std::fs::File,
        token_symbols: &HashMap<&str, &str>,
        has_rule_binding: bool,
        name: &str,
        regex: Regex,
    ) -> std::io::Result<()> {
        output.write_all(
            b"        let lhs = self.cst.mark();\
            \n        let mut m = None;\n",
        )?;
        Self::output_node_kind_decl(output, has_rule_binding, name, 2, true)?;
        Self::output_regex(cst, sema, regex, output, 2, token_symbols, true, name)?;
        output.write_all(b"        if let Some(m) = m {\n")?;
        Self::output_cst_close(output, has_rule_binding, name, 3, false)?;
        output.write_all(b"        }\n")
    }

    fn output_rule(
        cst: &Cst,
        sema: &SemanticData,
        rule: RuleDecl,
        output: &mut std::fs::File,
        token_symbols: &HashMap<&str, &str>,
    ) -> std::io::Result<()> {
        if !sema.used.contains(&rule.syntax()) {
            // don't generate code for unused rules
            return Ok(());
        }
        let name = rule.name(cst).unwrap().0;
        let pattern = sema.patterns.get(&rule);
        let is_start = sema.start.unwrap() == rule;
        let has_rule_binding = sema.has_rule_binding.contains(&rule);

        output.write_all(
            format!(
                "    {}fn r#{name}(&mut self, diags: &mut Vec<Diagnostic>) {{\n",
                if has_rule_binding {
                    "#[allow(unused_assignments)]\n    "
                } else {
                    ""
                }
            )
            .as_bytes(),
        )?;
        if let Some(regex) = rule.regex(cst) {
            match pattern {
                None => Self::output_normal_rule(
                    cst,
                    sema,
                    output,
                    token_symbols,
                    has_rule_binding,
                    name,
                    regex,
                    is_start,
                )?,
                Some(Pattern::LeftRecursive(branches)) => Self::output_left_recursive_rule(
                    cst,
                    sema,
                    output,
                    token_symbols,
                    has_rule_binding,
                    name,
                    regex,
                    branches,
                )?,
                Some(Pattern::OperatorPrecedence(branches)) => {
                    Self::output_operator_precedence_rule(cst, sema, output, name, regex, branches)?
                }
                Some(Pattern::UnconditionalForwarding) => {
                    Self::output_regex(cst, sema, regex, output, 2, token_symbols, false, name)?;
                }
                Some(Pattern::ConditionalForwarding) => {
                    Self::output_conditional_forwarding_rule(
                        cst,
                        sema,
                        output,
                        token_symbols,
                        has_rule_binding,
                        name,
                        regex,
                    )?;
                }
                Some(Pattern::MaybeEmpty) => {
                    Self::output_maybe_empty_rule(
                        cst,
                        sema,
                        output,
                        token_symbols,
                        has_rule_binding,
                        name,
                        regex,
                    )?;
                }
                Some(Pattern::RightRecursiveForwarding(branches)) => {
                    Self::output_right_recursice_forwarding_rule(
                        cst,
                        sema,
                        output,
                        token_symbols,
                        has_rule_binding,
                        name,
                        regex,
                        branches,
                    )?;
                }
            }
        }
        output.write_all(b"    }\n")?;
        Ok(())
    }

    fn get_predicate(cst: &Cst, rule_name: &str, regex: Regex) -> String {
        match regex {
            Regex::Concat(concat) => match concat.operands(cst).next().unwrap() {
                Regex::Predicate(pred) => {
                    format!(
                        " if self.predicate_{rule_name}_{}()",
                        &pred.value(cst).unwrap().0[1..]
                    )
                }
                _ => "".to_string(),
            },
            Regex::Paren(paren) => Self::get_predicate(cst, rule_name, paren.inner(cst).unwrap()),
            _ => "".to_string(),
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn output_regex(
        cst: &Cst,
        sema: &SemanticData,
        regex: Regex,
        output: &mut std::fs::File,
        level: usize,
        token_symbols: &HashMap<&str, &str>,
        open_before: bool,
        rule_name: &str,
    ) -> std::io::Result<()> {
        match regex {
            Regex::Name(name) => {
                let decl = sema.decl_bindings[&name.syntax()];
                if let Some(rule) = RuleDecl::cast(cst, decl) {
                    let name = rule.name(cst).unwrap().0;
                    output
                        .write_all(format!("self.r#{name}(diags);\n").indent(level).as_bytes())?;
                } else if let Some(token) = TokenDecl::cast(cst, decl) {
                    let name = token.name(cst).unwrap().0;
                    let sym = token
                        .symbol(cst)
                        .map_or(name, |(sym, _)| &sym[1..sym.len() - 1]);
                    output.write_all(
                        format!("expect!({name}, \"{sym}\", self, diags);\n",)
                            .indent(level)
                            .as_bytes(),
                    )?;
                }
            }
            Regex::Symbol(sym) => {
                let decl = sema.decl_bindings[&sym.syntax()];
                if let Some(token) = TokenDecl::cast(cst, decl) {
                    let name = token.name(cst).unwrap().0;
                    let sym = token.symbol(cst).unwrap().0;
                    let sym = &sym[1..sym.len() - 1];
                    output.write_all(
                        format!("expect!({name}, \"{sym}\", self, diags);\n",)
                            .indent(level)
                            .as_bytes(),
                    )?;
                }
            }
            Regex::Concat(concat) => {
                for op in concat.operands(cst) {
                    Self::output_regex(
                        cst,
                        sema,
                        op,
                        output,
                        level,
                        token_symbols,
                        false,
                        rule_name,
                    )?;
                }
            }
            Regex::Alternation(alt) => {
                output.write_all("match self.current {\n".indent(level).as_bytes())?;
                for op in alt.operands(cst) {
                    output.write_all(
                        format!(
                            "{}{} => {{\n",
                            sema.predict_sets[&op.syntax()].pattern(0),
                            Self::get_predicate(cst, rule_name, op)
                        )
                        .indent(level + 1)
                        .as_bytes(),
                    )?;
                    Self::output_regex(
                        cst,
                        sema,
                        op,
                        output,
                        level + 2,
                        token_symbols,
                        false,
                        rule_name,
                    )?;
                    output.write_all("}\n".indent(level + 1).as_bytes())?;
                }
                output.write_all(
                    format!(
                        "    _ => {{\
                       \n        self.error(diags, err![self.span(), {}]);\
                       \n    }}\
                       \n}}\n",
                        sema.predict_sets[&regex.syntax()].error(5, token_symbols),
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
            }
            Regex::Star(star) => {
                let op = star.operand(cst).unwrap();
                output.write_all(
                    format!(
                        "loop {{\
                       \n    match self.current {{\
                       \n        {}{} => {{\n",
                        sema.first_sets[&op.syntax()].pattern(2),
                        Self::get_predicate(cst, rule_name, op)
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
                if open_before {
                    output.write_all(
                        "if m.is_none() {\
                       \n    m = Some(self.cst.open_before(lhs));\
                       \n}\n"
                            .indent(5)
                            .as_bytes(),
                    )?;
                }
                Self::output_regex(
                    cst,
                    sema,
                    op,
                    output,
                    level + 3,
                    token_symbols,
                    false,
                    rule_name,
                )?;
                let recovery = &sema.recovery_sets[&regex.syntax()];
                output.write_all(
                    format!(
                        "        }}\
                       \n        {}{}{} => break,\
                       \n        _ => {{\
                       \n            self.advance_with_error(diags, err![self.span(), {}]);\
                       \n        }}\
                       \n    }}\
                       \n}}\n",
                        sema.follow_sets[&regex.syntax()].pattern(2),
                        if recovery.is_empty() {
                            ""
                        } else {
                            "\n        | "
                        },
                        recovery.pattern(2),
                        sema.predict_sets[&regex.syntax()].error(5, token_symbols),
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
            }
            Regex::Plus(plus) => {
                let op = plus.operand(cst).unwrap();
                Self::output_regex(
                    cst,
                    sema,
                    op,
                    output,
                    level,
                    token_symbols,
                    false,
                    rule_name,
                )?;
                output.write_all(
                    format!(
                        "loop {{\
                       \n    match self.current {{\
                       \n        {}{} => {{\n",
                        sema.first_sets[&op.syntax()].pattern(2),
                        Self::get_predicate(cst, rule_name, op)
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
                if open_before {
                    output.write_all(
                        "if m.is_none() {\
                       \n    m = Some(self.cst.open_before(lhs));\
                       \n}\n"
                            .indent(5)
                            .as_bytes(),
                    )?;
                }
                Self::output_regex(
                    cst,
                    sema,
                    op,
                    output,
                    level + 3,
                    token_symbols,
                    false,
                    rule_name,
                )?;
                let recovery = &sema.recovery_sets[&regex.syntax()];
                output.write_all(
                    format!(
                        "        }}\
                       \n        {}{}{} => break,\
                       \n        _ => {{\
                       \n            self.advance_with_error(diags, err![self.span(), {}]);\
                       \n        }}\
                       \n    }}\
                       \n}}\n",
                        sema.follow_sets[&regex.syntax()].pattern(2),
                        if recovery.is_empty() {
                            ""
                        } else {
                            "\n        | "
                        },
                        recovery.pattern(2),
                        sema.predict_sets[&regex.syntax()].error(5, token_symbols),
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
            }
            Regex::Optional(opt) => {
                let op = opt.operand(cst).unwrap();
                output.write_all(
                    format!(
                        "match self.current {{\
                       \n    {}{} => {{\n",
                        sema.first_sets[&op.syntax()].pattern(1),
                        Self::get_predicate(cst, rule_name, op)
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
                if open_before {
                    output.write_all(
                        "if m.is_none() {\
                       \n    m = Some(self.cst.open_before(lhs));\
                       \n}\n"
                            .indent(4)
                            .as_bytes(),
                    )?;
                }
                Self::output_regex(
                    cst,
                    sema,
                    op,
                    output,
                    level + 2,
                    token_symbols,
                    false,
                    rule_name,
                )?;
                output.write_all(
                    format!(
                        "    }}\
                       \n    {} => {{}}\
                       \n    _ => {{\
                       \n        self.error(diags, err![self.span(), {}]);\
                       \n    }}\
                       \n}}\n",
                        sema.follow_sets[&regex.syntax()].pattern(1),
                        sema.predict_sets[&regex.syntax()].error(5, token_symbols),
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
            }
            Regex::Paren(paren) => {
                Self::output_regex(
                    cst,
                    sema,
                    paren.inner(cst).unwrap(),
                    output,
                    level,
                    token_symbols,
                    false,
                    rule_name,
                )?;
            }
            Regex::Action(action) => {
                output.write_all(
                    format!(
                        "self.action_{rule_name}_{}(diags);\n",
                        &action.value(cst).unwrap().0[1..]
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
            }
            Regex::Binding(bind) => {
                let name = &bind.value(cst).unwrap().0[1..];
                if !name.is_empty() {
                    output.write_all(
                        format!("node_kind = Rule::{};\n", Self::snake_to_pascal_case(name))
                            .indent(level)
                            .as_bytes(),
                    )?;
                }
            }
            Regex::OpenNode(open) => {
                let number = open.number(cst).unwrap();
                output.write_all(
                    format!("let m{number} = self.cst.mark();\n")
                        .indent(level)
                        .as_bytes(),
                )?;
            }
            Regex::CloseNode(close) => {
                let number = close.number(cst).unwrap();
                let node_name = close.node_name(cst).unwrap();
                output.write_all(
                    format!(
                        "let open_node = self.cst.open_before(m{number});\
                        \nself.close(open_node, Rule::{}, diags);\n",
                        Self::snake_to_pascal_case(node_name)
                    )
                    .indent(level)
                    .as_bytes(),
                )?;
            }
            _ => {}
        }
        Ok(())
    }

    fn snake_to_pascal_case(name: &str) -> String {
        let mut res = String::new();
        let mut upper = true;
        for c in name.chars() {
            if upper {
                res.push(c.to_ascii_uppercase());
                upper = false;
            } else if c == '_' {
                upper = true;
            } else {
                res.push(c);
            }
        }
        res
    }

    /// Outputs the Parser struct and impl.
    fn output_generated(
        cst: &Cst,
        sema: &SemanticData,
        file: File,
        output: &mut std::fs::File,
    ) -> std::io::Result<()> {
        let mut token_symbols = HashMap::from([("EOF", "<end of file>")]);
        for token in file.token_decls(cst) {
            let name = token.name(cst).unwrap().0;
            let sym = token
                .symbol(cst)
                .map_or(name, |(sym, _)| &sym[1..sym.len() - 1]);
            token_symbols.insert(name, sym);
        }
        let mut rules = "".to_string();
        let mut rule_names = HashSet::new();
        for rule in file.rule_decls(cst) {
            let rule_name = rule.name(cst).unwrap().0;
            rule_names.insert(rule_name);
            rules += "\n    ";
            rules += &Self::snake_to_pascal_case(rule_name);
            rules += ",";
        }
        for rule_name in sema.rule_bindings.iter() {
            if rule_names.contains(rule_name) {
                continue;
            }
            rules += "\n    ";
            rules += &Self::snake_to_pascal_case(rule_name);
            rules += ",";
        }
        let mut skip = "".to_string();
        for token in sema.skipped.iter() {
            skip += " | Token::";
            skip += token.name(cst).unwrap().0;
        }

        output.write_all(
            format!(
                include_str!("../skeleton/generated.rs"),
                rules,
                skip,
                sema.start.unwrap().name(cst).unwrap().0
            )
            .as_bytes(),
        )?;
        for rule in file.rule_decls(cst) {
            Self::output_rule(cst, sema, rule, output, &token_symbols)?;
        }
        output.write_all(b"}\n\n")?;

        Self::output_predicates_and_actions(output, sema, true)
    }
}
