use std::{collections::HashMap, ops::Range, rc::Rc};

use libterm::Color;
use regex::Regex;

use crate::buffer::{line::Line, style::Style};

#[derive(Default)]
pub struct Highlighter {
    syntaxes: HashMap<Rc<str>, Syntax>,
}

pub struct TokenSpan {
    pub range: Range<usize>,
    pub category: Rc<str>,
}

#[derive(Default)]
pub struct LineHighlight {
    pub start_state: u32,
    pub end_state: u32,
    pub tokens: Vec<TokenSpan>,
    pub dirty: bool,
}

enum SyntaxPattern {
    Keyword(Rc<str>),
    Regex(Regex),
}

pub struct SyntaxRule {
    pattern: SyntaxPattern,
    next_state: Option<u32>,
    category: Rc<str>,
}

#[derive(Default)]
pub struct SyntaxStyle {
    pub foreground: Option<Color>,
    pub background: Option<Color>,
    pub bold: Option<bool>,
}

#[derive(Default)]
pub struct Syntax {
    state_rules: HashMap<u32, Vec<SyntaxRule>>,
    style_rules: HashMap<Rc<str>, SyntaxStyle>,
}

impl SyntaxStyle {
    pub fn apply(&self, base: &Style) -> Style {
        let mut result = *base;
        if let Some(foreground) = self.foreground {
            result.foreground = foreground;
        }
        if let Some(background) = self.background {
            result.background = background;
        }
        if let Some(bold) = self.bold {
            result.bold = bold;
        }
        result
    }
}

impl SyntaxRule {
    pub fn keyword<S: Into<Rc<str>>, C: Into<Rc<str>>>(
        keyword: S,
        next_state: Option<u32>,
        category: C,
    ) -> Self {
        Self {
            pattern: SyntaxPattern::Keyword(keyword.into()),
            next_state,
            category: category.into(),
        }
    }

    pub fn regex<C: Into<Rc<str>>>(
        regex: &str,
        next_state: Option<u32>,
        category: C,
    ) -> Option<Self> {
        Some(Self {
            pattern: SyntaxPattern::Regex(Regex::new(regex).ok()?),
            next_state,
            category: category.into(),
        })
    }
}

impl Highlighter {
    pub fn define_rule(&mut self, filetype: Rc<str>, entry_state: u32, rule: SyntaxRule) {
        self.syntaxes
            .entry(filetype)
            .or_default()
            .define_rule(entry_state, rule);
    }

    pub fn define_category(&mut self, filetype: Rc<str>, category: Rc<str>, style: SyntaxStyle) {
        self.syntaxes
            .entry(filetype)
            .or_default()
            .define_style(category, style);
    }

    pub fn stylize(&self, filetype: &str, category: &str, base: &Style) -> Option<Style> {
        self.syntaxes
            .get(filetype)
            .and_then(|syntax| syntax.stylize(category, base))
    }

    pub fn rehighlight(&self, lines: &mut [Line], filetype: &str) {
        // Ignore if no syntax is defined
        let Some(syntax) = self.syntaxes.get(filetype) else {
            return;
        };

        let mut dirty_start = None;
        let mut dirty_end = None;
        for (i, line) in lines.iter().enumerate() {
            if line.highlight_dirty {
                if dirty_start.is_none() {
                    dirty_start = Some(i);
                }
                dirty_end = Some(i);
            }
        }
        let Some(dirty_start) = dirty_start else {
            return;
        };
        let dirty_end = dirty_end.unwrap();

        let mut index = dirty_start;
        let mut expected_start_state = if index == 0 {
            0
        } else {
            lines[index - 1].highlight.end_state
        };

        // Keep rehighlighting lines until either both !dirty and in the same state or
        // until the end of the file is reached
        while index < lines.len() {
            let is_dirty = lines[index].highlight_dirty;
            let state_mismatch = lines[index].highlight.start_state != expected_start_state;

            if is_dirty || state_mismatch {
                lines[index].highlight.start_state = expected_start_state;
                syntax.highlight(&mut lines[index]);
                lines[index].highlight_dirty = false;
            } else if index > dirty_end {
                break;
            }

            expected_start_state = lines[index].highlight.end_state;
            index += 1;
        }
    }
}

impl Syntax {
    pub fn new() -> Self {
        Self {
            state_rules: HashMap::new(),
            style_rules: HashMap::new(),
        }
    }

    pub fn define_style<C: Into<Rc<str>>>(&mut self, category: C, style: SyntaxStyle) {
        self.style_rules.insert(category.into(), style);
    }

    pub fn define_rule(&mut self, state: u32, rule: SyntaxRule) {
        self.state_rules.entry(state).or_default().push(rule);
    }

    // pub fn highlight(&self, line: &Text, state: &mut LineHighlight) {

    fn is_word_char(c: char) -> bool {
        !c.is_whitespace() && !"()[]{},<>".contains(c)
    }

    pub fn stylize(&self, category: &str, base: &Style) -> Option<Style> {
        self.style_rules
            .get(category)
            .map(|style| style.apply(base))
    }

    pub fn highlight(&self, line: &mut Line) {
        line.highlight.tokens.clear();
        let text = line.to_string();
        let mut cursor = 0;
        let mut current_state = line.highlight.start_state;

        while cursor < text.len() {
            let Some(rule_set) = self.state_rules.get(&current_state) else {
                break;
            };

            let current_slice = &text[cursor..];

            let mut matched = false;
            for rule in rule_set {
                match &rule.pattern {
                    SyntaxPattern::Regex(regex) => {
                        // TODO this is inefficient
                        if let Some(regex_match) = regex.find(current_slice)
                            && regex_match.start() == 0
                        {
                            let match_len = regex_match.end();

                            line.highlight.tokens.push(TokenSpan {
                                range: cursor..cursor + match_len,
                                category: rule.category.clone(),
                            });

                            cursor += match_len;
                            if let Some(next) = rule.next_state {
                                current_state = next;
                            }
                            matched = true;
                            break;
                        }
                    }
                    SyntaxPattern::Keyword(keyword) => {
                        if current_slice.starts_with(keyword.as_ref()) {
                            let pattern_len = keyword.len();
                            let pattern_starts_with_word =
                                keyword.chars().next().is_none_or(Self::is_word_char);
                            let pattern_ends_with_word =
                                keyword.chars().next_back().is_none_or(Self::is_word_char);

                            let leading_boundary_match = !pattern_starts_with_word || {
                                text[..cursor]
                                    .chars()
                                    .next_back()
                                    .is_none_or(|c| !Self::is_word_char(c))
                            };
                            let trailing_boundary_match = !pattern_ends_with_word || {
                                text[cursor + pattern_len..]
                                    .chars()
                                    .next()
                                    .is_none_or(|c| !Self::is_word_char(c))
                            };

                            if leading_boundary_match && trailing_boundary_match {
                                line.highlight.tokens.push(TokenSpan {
                                    range: cursor..cursor + pattern_len,
                                    category: rule.category.clone(),
                                });
                                cursor += pattern_len;
                                if let Some(next) = rule.next_state {
                                    current_state = next;
                                }
                                matched = true;
                                break;
                            }
                        }
                    }
                }
            }
            if !matched {
                cursor = text.ceil_char_boundary(cursor + 1);
            }
        }

        line.highlight.end_state = current_state;
    }
}
