use std::collections::HashSet;
use std::sync::Arc;

use bit_set::BitSet;

use crate::atn::ATN;
use crate::atn_config::ATNConfig;
use crate::atn_state::ATNState;
use crate::atn_state::ATNStateRef;
use crate::interval_set::IntervalSet;
use crate::prediction_context::PredictionContext;
use crate::prediction_context::EMPTY_PREDICTION_CONTEXT;
use crate::token::{TOKEN_EOF, TOKEN_EPSILON, TOKEN_INVALID_TYPE, TOKEN_MIN_USER_TOKEN_TYPE};
use crate::transition::Transition;
use crate::tree::RuleNode;

pub struct LL1Analyzer<'a> {
    atn: &'a ATN,
}

impl LL1Analyzer<'_> {
    pub fn new(atn: &ATN) -> LL1Analyzer<'_> {
        LL1Analyzer { atn }
    }

    //    fn get_decision_lookahead(&self, _s: &dyn ATNState) -> &Vec<IntervalSet> { unimplemented!() }

    pub fn look<'input, 'arena, Node>(
        &self,
        s: ATNStateRef,
        stop_state: Option<ATNStateRef>,
        ctx: Option<&'arena Node>,
    ) -> IntervalSet
    where
        'input: 'arena,
        Node: RuleNode<'input, 'arena>,
    {
        let mut r = IntervalSet::new();
        let look_ctx = ctx.map(|x| PredictionContext::from_rule_context(self.atn, x));
        let mut looks_busy: HashSet<ATNConfig> = HashSet::new();
        let mut called_rule_stack = BitSet::new();
        self.look_work(
            s,
            stop_state,
            look_ctx,
            &mut r,
            &mut looks_busy,
            &mut called_rule_stack,
            true,
            true,
        );
        r
    }

    #[allow(clippy::too_many_arguments)]
    fn look_work(
        &self,
        s: ATNStateRef,
        stop_state: Option<ATNStateRef>,
        ctx: Option<Arc<PredictionContext>>,
        look: &mut IntervalSet,
        look_busy: &mut HashSet<ATNConfig>,
        called_rule_stack: &mut BitSet,
        see_thru_preds: bool,
        add_eof: bool,
    ) {
        let c = ATNConfig::new(s, 0, ctx.clone());
        if !look_busy.insert(c) {
            return;
        }

        if Some(s.get_state_number()) == stop_state.map(|x| x.get_state_number()) {
            match ctx {
                None => {
                    look.add_one(TOKEN_EPSILON);
                    return;
                }
                Some(x) if x.is_empty() && add_eof => {
                    look.add_one(TOKEN_EOF);
                    return;
                }
                _ => {}
            }
        }

        if let ATNState::RuleStop(_) = *s {
            match ctx {
                None => {
                    look.add_one(TOKEN_EPSILON);
                    return;
                }
                Some(x) if x.is_empty() && add_eof => {
                    look.add_one(TOKEN_EOF);
                    return;
                }
                Some(ctx) if ctx != *EMPTY_PREDICTION_CONTEXT => {
                    let removed = called_rule_stack.contains(s.get_rule_index() as usize);
                    called_rule_stack.remove(s.get_rule_index() as usize);
                    for i in 0..ctx.length() {
                        self.look_work(
                            ctx.get_return_state(i),
                            stop_state,
                            ctx.get_parent(i).cloned(),
                            look,
                            look_busy,
                            called_rule_stack,
                            see_thru_preds,
                            add_eof,
                        )
                    }
                    if removed {
                        called_rule_stack.insert(s.get_rule_index() as usize);
                    }

                    return;
                }
                _ => {}
            }
        }

        for tr in s.get_transitions() {
            let target = tr.get_target();
            match tr {
                Transition::Rule(rule_tr) => {
                    if called_rule_stack.contains(target.get_rule_index() as usize) {
                        continue;
                    }

                    let new_ctx = Arc::new(PredictionContext::new_singleton(
                        ctx.clone(),
                        rule_tr.follow_state,
                    ));

                    called_rule_stack.insert(target.get_rule_index() as usize);
                    self.look_work(
                        target,
                        stop_state,
                        Some(new_ctx),
                        look,
                        look_busy,
                        called_rule_stack,
                        see_thru_preds,
                        add_eof,
                    );
                    called_rule_stack.remove(target.get_rule_index() as usize);
                }
                Transition::Predicate(_) | Transition::PrecedencePredicate(_) => {
                    if see_thru_preds {
                        self.look_work(
                            target,
                            stop_state,
                            ctx.clone(),
                            look,
                            look_busy,
                            called_rule_stack,
                            see_thru_preds,
                            add_eof,
                        )
                    } else {
                        look.add_one(TOKEN_INVALID_TYPE);
                    }
                }
                Transition::Wildcard(_) => {
                    look.add_range(TOKEN_MIN_USER_TOKEN_TYPE, self.atn.max_token_type)
                }
                _ if tr.is_epsilon() => self.look_work(
                    target,
                    stop_state,
                    ctx.clone(),
                    look,
                    look_busy,
                    called_rule_stack,
                    see_thru_preds,
                    add_eof,
                ),
                _ => {
                    if let Some(set) = tr.get_label() {
                        if matches!(tr, Transition::NotSet(_)) {
                            look.add_set(
                                &set.complement(TOKEN_MIN_USER_TOKEN_TYPE, self.atn.max_token_type),
                            );
                        } else {
                            look.add_set(set)
                        }
                    }
                }
            }
        }
    }
}
