// Copyright (c) 2025 Redglyph (@gmail.com). All Rights Reserved.

#![cfg(test)]

use iter_index::IndexerIterator;
use lexigram_lib::{CollectJoin, SymbolTable, VarId};
use lexigram_lib::build::{BuildFrom, BuildInto};
use lexigram_lib::lexergen::{LexerGen, LexerTables};
use lexigram_lib::log::{LogReader, LogStatus};
use crate::Lexi;
use crate::lexi::SymbolicDfa;

static TXT_LEXI1: &str = r#"
    lexicon A;
    //
    fragment ID     : [a-zA-Z][a-zA-Z_0-9]*;
    fragment NUM    : [0-9]+;
    fragment WS     : [ \n\r\t]+;
    //
    WhiteSpace      : WS -> skip;
    Comment         : '/*' .*? '*/' -> skip;
    LineComment     : '//' ~[\r\n]* -> skip;
    Add             : '+';
    Equal           : '=';
    Lparen          : '(';
    Rparen          : ')';
    Semicolon       : ';';
    Sub             : '-';
    Else            : 'else';
    If              : 'if';
    Let             : 'let';
    Print           : 'print';
    Id              : ID;
    Int             : NUM;
"#;

static TXT_GRAM1: &str = r#"
    grammar A;
    /**---- comments ----**/
    prog:
        instr+;                             // [Plus]
    instr:
        Let Id Equal expr Semicolon
    |   Print expr Semicolon
    |   empty_instr Semicolon
    |   If Lparen expr Rparen instr (<G> Else instr)?; // <G> greedy "else" to avoid warning
    empty_instr:
        Sub empty_instr<L>                  // [<L>] on right recursion
    |   ;                                   // [prodFactor: prodTerm*] when empty -> ε
    expr:
        term ((Add|Sub) term)*;             // [Star] and [|] inside Plus/Star (RTS to PRS)
    term:
        Id | Sub? Int | Lparen expr Rparen; // [?]
"#;

static TXT_GRAM2: &str = r#"
    grammar B;
    tests: (test Semicolon)*;
    test: a | b | c;
    a: <L=a> Add Id a |;                    // [<L=own Id>] on right recursion
    b: (<L=b_iter> Sub Id)+;                // [<L=Id>] inside Plus/Star
    c: Let Id Equal Int <R>;                // [<R>] (not used yet in ParserGen)
"#;

fn make_lexer_tables(lexicon: &str) -> (LexerTables, SymbolTable) {
    const VERBOSE: bool = false;

    // parses the test lexicon
    let lexi = Lexi::new(lexicon);
    let symbolic_dfa: SymbolicDfa = lexi.build_into();
    let SymbolicDfa { dfa, symbol_table, .. } = symbolic_dfa;
    let msg = dfa.get_log().get_messages_str();
    assert!(dfa.get_log().has_no_errors(), "couldn't parse the lexicon:\n{msg}");
    if VERBOSE && !msg.is_empty() {
        println!("Lexi messages:\n{msg}");
    }

    // builds the lexer for the test lexicon
    if VERBOSE {
        println!("Lexer symbol table:\n- T: {}",
                 symbol_table.get_terminals().index::<VarId>()
                     .map(|(v, (t, maybe))| format!("{v}:{t}{}", if let Some(s) = maybe { format!("='{s}'") } else { String::new() }))
                     .join(", "));
    }
    let lexer_tables = LexerTables::build_from(LexerGen::build_from(dfa));
    (lexer_tables, symbol_table)
}

mod listener {
    use super::*;
    use crate::Gram;
    use lexigram_lib::build::{BuildFrom, BuildInto};
    use lexigram_lib::log::{BufLog, LogReader, LogStatus, Logger};
    use lexigram_lib::parser::{Call, ListenerWrapper};
    use lexigram_lib::grammar::ProdRuleSet;
    use lexigram_lib::char_reader::CharReader;
    use lexigram_lib::lexer::TokenSpliterator;
    use lexigram_lib::parsergen::{ParserGen, ParserTables};
    use lexigram_lib::{AltId, VarId, LL1};
    use lexigram_lib::CollectJoin;

    struct Stub {
        log: BufLog,
        verbose: bool
    }

    impl Stub {
        pub fn new() -> Self {
            Stub { log: BufLog::new(), verbose: false }
        }

        pub fn set_verbose(&mut self, verbose: bool) {
            self.verbose = verbose;
        }

        pub fn get_log(&self) -> &BufLog {
            &self.log
        }
    }

    impl ListenerWrapper for Stub {
        fn switch(&mut self, call: Call, nt: VarId, alt_id: AltId, t_data: Option<Vec<String>>) {
            if self.verbose {
                println!(":: {call:?} nt={nt} alt_id={alt_id}{}",
                         if let Some(t) = t_data { format!(" - {}", t.iter().join(",")) } else { String::new() });
            }
        }

        fn get_log_mut(&mut self) -> &mut impl Logger {
            &mut self.log
        }

    }

    #[test]
    fn just_parsing() {
        let tests = vec![
            (
                TXT_GRAM1,
                vec![], false,
                vec![
                    // (input, lexer ok, parser ok)
                    ("let a = 1; let b = 2; print a + (b - 5);", true, true),
                    ("let c = 10 + 11 + -12; ----;;", true, true),
                    ("if (a) print a; if (b) print b; else print c;", true, true),
                    ("let d = a; let e = d + 1 print e; print d; print a", true, false),
                    ("", false, false),
                    ("( print x;", true, false),
                ]
            ),
            (
                TXT_GRAM2,
                vec![], true,
                vec![
                    ("/* a */; + a; + a + b; /* b */ - a; -a-b; // 1st line\n/* c */ let x=2; // end", true, true),
                ]
            ),
            (
                "grammar T2; a: <L=Id> Id;           // error: [<L=Id>] with Id of terminal",
                vec!["rule name in <L=Id> is already defined as terminal", "abort request"], false,
                vec![]
            ),
            (
                "grammar T3; a: b;                   // error: undefined symbol",
                vec!["has been used but is not defined, neither as terminal or non-terminal"], false,
                vec![]
            ),
            (
                "grammar T4; Id: Int Plus Int;       // error: terminal used as rule name",
                vec!["is a terminal and cannot be used as a rule name"], false,
                vec![]
            ),
            (
                "grammar T5; a: Int; a: Plus Int;    // error: non-terminal already defined",
                vec!["non-terminal 'a' already defined"], false,
                vec![]
            ),
            (
                "grammar T6; EOF: Plus Int;          // EOF as rule name, should be interesting",
                vec!["found input 'EOF' instead of 'Id'", "found input 'Id' instead of ':'"], false,
                vec![]
            ),
            (
                "grammar T7; a: b* EOF; b: Id ( Equal Int )? Semicolon;",
                vec![], true,
                vec![
                    ("a; b = 1; c = 2;", true, true),
                ]
            ),
            (
                "grammar T8; a: b*; b: Id EOF;       // EOF can only be in the first rule",
                vec!["EOF can only be put in the top rule"], false,
                vec![]
            ),
            (
                "grammar T9; a: Add | b | ; b: c | Equal d; c: |; d:;",
                vec![], true,
                vec![(" + ", true, true), (" = ", true, true)]
            ),
            (
                r#"grammar T10;
                a: Lparen (<L=a> Id)* Rparen;
                "#,
                vec!["<L=a> uses the rule nonterminal instead of a new one for the loop"], false,
                vec![]
            ),
            (
                r#"grammar T11;
                a: Lparen (<L=i1> <L=i2> Id <L=i3>)* Rparen;
                "#,
                vec!["extra <L>: <L=i1> was already declared in this scope"], false,
                vec![]
            ),
            (
                r#"grammar T12;
                a: Lparen (<L=i1> Int | <L=i2> Id | <L=i3> Add)* Rparen;
                "#,
                vec![
                    "extra <L=i2>, <L=i1> was already declared in this scope",
                    "extra <L=i3>, <L=i1> was already declared in this scope"
                ], false,
                vec![]
            ),
            (
                r#"grammar T13;
                a: Lparen (<L=i> Id)* Rparen;
                "#,
                vec![], true,
                vec![]
            ),
        ];
        const VERBOSE: bool = false;
        const VERBOSE_WRAPPER: bool = false;

        let (lexer_tables, symbol_table) = make_lexer_tables(TXT_LEXI1);
        let mut lexer = lexer_tables.make_lexer();

        for (test_id, (grammar, mut expected_grammar_errors, expected_warnings, inputs)) in tests.into_iter().enumerate() {
            if VERBOSE { println!("{:=<80}\ntest {test_id}", ""); }
            let text = format!("test {test_id} failed");

            // grammar parser
            let gram = Gram::new(symbol_table.clone(), grammar);
            let ll1: ProdRuleSet<LL1> = gram.build_into();
            let msg = ll1.get_log().get_messages_str();
            let should_succeed = expected_grammar_errors.is_empty();
            if VERBOSE {
                ll1.print_alts();
                println!("Gram messages:\n{msg}");
            }
            assert_eq!(ll1.get_log().has_no_errors(), should_succeed,
                       "{text}: was expecting grammar parsing to {}:\n{msg}", if expected_grammar_errors.is_empty() { "succeed" } else { "fail" });
            for m in ll1.get_log().get_errors() {
                let mut i = 0;
                while i < expected_grammar_errors.len() {
                    if m.get_inner_str().contains(expected_grammar_errors[i]) {
                        expected_grammar_errors.remove(i);
                        break;
                    } else {
                        i += 1;
                    }
                }
                if expected_grammar_errors.is_empty() { break }
            }
            assert!(expected_grammar_errors.is_empty(), "was expecting to find those errors while parsing the grammar:{}\nbut got those messages:\n{msg}",
                    expected_grammar_errors.iter().map(|s| format!("\n- {s}")).join(""));
            if should_succeed {
                let builder = ParserGen::build_from(ll1);
                let msg = builder.get_log().get_messages_str();
                if VERBOSE {
                    builder.print_flags(4);
                    println!("Parsing table of grammar '{}':", builder.get_name());
                    builder.get_parsing_table().print(builder.get_symbol_table(), 4);
                    if !builder.get_log().is_empty() {
                        println!("Messages:\n{msg}");
                    }
                }
                assert_eq!(builder.get_log().num_warnings() > 0, expected_warnings, "{} warnings:\n{msg}", if expected_warnings { "Expected" } else { "Didn't expect"} );
                let parser_table = ParserTables::build_from(builder);
                let mut parser = parser_table.make_parser();

                for (input, expected_lexer_success, expected_parser_success) in inputs {
                    if VERBOSE { println!("- input '{input}'"); }
                    let input_stream = CharReader::new(input.as_bytes());
                    lexer.attach_stream(input_stream);
                    let mut stub = Stub::new();
                    stub.set_verbose(VERBOSE_WRAPPER);
                    let result_parser = parser.parse_stream(&mut stub, lexer.tokens().keep_channel0());
                    let msg = format!("{text}, input '{input}'");
                    if VERBOSE && !stub.get_log().is_empty() {
                        println!("  Messages:{}", stub.get_log().get_messages().map(|m| format!("\n  - {m}")).join(""));
                    }
                    assert_eq!(result_parser.is_ok(), expected_parser_success, "{msg}: {result_parser:?} instead of {}", if expected_parser_success { "success" } else { "failure" });
                    assert_eq!(!lexer.has_error(), expected_lexer_success, "{msg}: lexer errors: {}", lexer.get_error());
                }
            }
        }
    }

    #[ignore]
    #[test]
    fn nt_order() {
        const VERBOSE: bool = false;

        let tests = vec![TXT_GRAM2];

        let (_lexer_tables, symbol_table) = make_lexer_tables(TXT_LEXI1);

        for (test_id, grammar) in tests.into_iter().enumerate() {
            if VERBOSE { println!("{:=<80}\ntest {test_id}", ""); }
            let _text = format!("test {test_id} failed");

            // grammar parser
            let gram = Gram::new(symbol_table.clone(), grammar);
            let ll1: ProdRuleSet<LL1> = gram.build_into();
            let msg = ll1.get_log().get_messages_str();
            println!("{msg}");
            println!("Nonterminals:\n{}", ll1.get_symbol_table().unwrap().get_nonterminals().enumerate().map(|(i, nt)| format!("- {i:2}: {nt}")).join("\n"));
            let out = ll1.prs_alt_origins_str(true);
            println!("{}", out.join("\n"));
        }
    }
}