use std::{ffi::OsStr, iter, ops::Range, path::Path};

#[derive(Debug, Clone)]
pub(crate) struct Syntax<'a> {
    pub(crate) filetype: &'a str,
    pub(crate) filematch: &'a [&'a str],
    pub(crate) number: bool,
    pub(crate) single_line_comment: &'a [&'a str],
    pub(crate) multi_line_comment: &'a [(&'a str, &'a str)],
    pub(crate) string_literal: &'a [(&'a str, &'a str, bool)],
    pub(crate) keyword1: &'a [&'a str],
    pub(crate) keyword2: &'a [&'a str],
}

const DEFAULT: Syntax = Syntax {
    filetype: "no ft",
    filematch: &[],
    number: false,
    single_line_comment: &[],
    multi_line_comment: &[],
    string_literal: &[],
    keyword1: &[],
    keyword2: &[],
};

const HLDB: &[Syntax] = &[
    Syntax {
        filetype: "c",
        filematch: &[".c", ".h", ".cpp"],
        number: true,
        single_line_comment: &["//"],
        multi_line_comment: &[("/*", "*/")],
        string_literal: &[
            ("'", "'", true),
            ("L'", "'", true),
            ("u'", "'", true),
            ("U'", "'", true),
            ("\"", "\"", true),
            ("u8\"", "\"", true),
            ("u\"", "\"", true),
            ("U\"", "\"", true),
            ("L\"", "\"", true),
        ],
        keyword1: &[
            "switch", "if", "while", "for", "break", "continue", "return", "else", "struct",
            "union", "typedef", "static", "enum", "class", "case",
        ],
        keyword2: &[
            "int", "long", "double", "float", "char", "unsigned", "signed", "void",
        ],
    },
    Syntax {
        filetype: "rust",
        filematch: &[".rs"],
        number: true,
        single_line_comment: &["//"],
        multi_line_comment: &[("/*", "*/")],
        string_literal: &[
            ("'", "'", true),
            ("b'", "'", true),
            ("\"", "\"", true),
            ("r\"", "\"", false),
            ("r#\"", "\"#", false),
            ("r##\"", "\"##", false),
            ("r###\"", "\"###", false),
            ("r####\"", "\"####", false),
            ("r#####\"", "\"#####", false),
        ],
        keyword1: &[
            "as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false",
            "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
            "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type",
            "unsafe", "use", "where", "while",
        ],
        keyword2: &[
            "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize", "bool", "char",
            "f32", "f64",
        ],
    },
];

impl<'s> Syntax<'s> {
    pub(crate) fn select(filename: Option<impl AsRef<Path>>) -> &'static Syntax<'static> {
        Self::select_from_hldb(filename).unwrap_or(&DEFAULT)
    }

    fn select_from_hldb(filename: Option<impl AsRef<Path>>) -> Option<&'static Syntax<'static>> {
        let filename = filename?;
        let filename = filename.as_ref();
        let name = filename.file_name();
        let ext = filename.extension();

        for syntax in HLDB {
            let is_match = syntax.filematch.iter().copied().any(|m| {
                let is_ext = m.starts_with('.');
                if is_ext {
                    ext == Some(OsStr::new(m.trim_start_matches('.')))
                } else {
                    name == Some(OsStr::new(m))
                }
            });
            if is_match {
                return Some(syntax);
            }
        }
        None
    }

    fn parse(
        &'s self,
        chars: &str,
        prev_sep: &mut bool,
        open: &mut Option<Open<'s>>,
    ) -> (Highlight, usize) {
        match open {
            Some(Open::String { end, has_escape }) => {
                let (len, new_open) = self.parse_string_literal_end(chars, end, *has_escape);
                *prev_sep = true;
                *open = new_open;
                (Highlight::String, len)
            }
            Some(Open::Comment { end }) => {
                let (len, new_open) = self.parse_multi_line_comment_end(chars, end);
                *prev_sep = true;
                *open = new_open;
                (Highlight::MultiLineComment, len)
            }
            None => {
                if let Some(len) = self.parse_single_line_comment(chars) {
                    *prev_sep = true;
                    (Highlight::SingleLineComment, len)
                } else if let Some((len, new_open)) = self.parse_multi_line_comment_start(chars) {
                    *prev_sep = true;
                    *open = Some(new_open);
                    (Highlight::MultiLineComment, len)
                } else if let Some((len, new_open)) = self.parse_string_literal_start(chars) {
                    *prev_sep = true;
                    *open = Some(new_open);
                    (Highlight::String, len)
                } else if let Some(len) = self.parse_number(chars, *prev_sep) {
                    *prev_sep = false;
                    (Highlight::Number, len)
                } else if let Some(len) = self.parse_keyword1(chars, *prev_sep) {
                    *prev_sep = false;
                    (Highlight::Keyword1, len)
                } else if let Some(len) = self.parse_keyword2(chars, *prev_sep) {
                    *prev_sep = false;
                    (Highlight::Keyword2, len)
                } else {
                    let ch = chars.chars().next().unwrap();
                    let hl = if ch.is_control() {
                        Highlight::ControlCharacter
                    } else {
                        Highlight::Normal
                    };
                    *prev_sep = is_separator(ch);
                    (hl, ch.len_utf8())
                }
            }
        }
    }

    fn parse_single_line_comment(&self, chars: &str) -> Option<usize> {
        for scs in self.single_line_comment {
            if chars.starts_with(scs) {
                return Some(chars.len());
            }
        }
        None
    }

    fn parse_multi_line_comment_start(&self, chars: &str) -> Option<(usize, Open)> {
        for (mcs, mce) in self.multi_line_comment {
            if chars.starts_with(mcs) {
                return Some((mcs.len(), Open::Comment { end: *mce }));
            }
        }
        None
    }

    fn parse_multi_line_comment_end<'a>(
        &self,
        chars: &str,
        mce: &'a str,
    ) -> (usize, Option<Open<'a>>) {
        if let Some((idx, _)) = chars.match_indices(mce).next() {
            (idx + mce.len(), None)
        } else {
            (chars.len(), Some(Open::Comment { end: mce }))
        }
    }

    fn parse_string_literal_start(&self, chars: &str) -> Option<(usize, Open)> {
        for (sls, sle, has_escape) in self.string_literal {
            if chars.starts_with(sls) {
                return Some((
                    sls.len(),
                    Open::String {
                        end: *sle,
                        has_escape: *has_escape,
                    },
                ));
            }
        }
        None
    }

    fn parse_string_literal_end<'a>(
        &self,
        chars: &str,
        sle: &'a str,
        has_escape: bool,
    ) -> (usize, Option<Open<'a>>) {
        let mut escaped = None;
        let sle_head = sle.chars().next().unwrap();
        for (idx, _) in chars.match_indices(&[sle_head, '\\'][..]) {
            if escaped == Some(idx) {
                continue;
            }
            if has_escape && chars[idx..].starts_with('\\') {
                escaped = Some(idx + '\\'.len_utf8());
                continue;
            }
            if chars[idx..].starts_with(sle) {
                return (idx + sle.len(), None);
            }
        }
        (
            chars.len(),
            Some(Open::String {
                end: sle,
                has_escape,
            }),
        )
    }

    fn parse_number(&self, chars: &str, prev_sep: bool) -> Option<usize> {
        if !prev_sep || !self.number {
            return None;
        }

        let t = chars.trim_start_matches(|ch: char| ch.is_digit(10));
        if chars.len() != t.len() {
            let t = t.trim_start_matches(|ch: char| ch.is_digit(10) || ch == '.');
            Some(chars.len() - t.len())
        } else {
            None
        }
    }

    fn parse_keyword_common(&self, chars: &str, prev_sep: bool, kws: &[&str]) -> Option<usize> {
        if !prev_sep {
            return None;
        }
        for kw in kws {
            if !chars.starts_with(kw) {
                continue;
            }
            let t = &chars[kw.len()..];
            if t.is_empty() || t.starts_with(is_separator) {
                return Some(kw.len());
            }
        }
        None
    }

    fn parse_keyword1(&self, chars: &str, prev_sep: bool) -> Option<usize> {
        self.parse_keyword_common(chars, prev_sep, self.keyword1)
    }
    fn parse_keyword2(&self, chars: &str, prev_sep: bool) -> Option<usize> {
        self.parse_keyword_common(chars, prev_sep, self.keyword2)
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum Highlight {
    Normal,
    ControlCharacter,
    SingleLineComment,
    MultiLineComment,
    Keyword1,
    Keyword2,
    String,
    Number,
    Match,
    LineMarker,
}

impl Highlight {
    pub(crate) fn to_color(self) -> (u32, u32) {
        match self {
            Self::Normal => (37, 47),
            Self::ControlCharacter => (30, 47),
            Self::SingleLineComment | Self::MultiLineComment => (36, 40),
            Self::Keyword1 => (33, 40),
            Self::Keyword2 => (32, 40),
            Self::String => (35, 40),
            Self::Number => (31, 40),
            Self::Match => (34, 40),
            Self::LineMarker => (37, 40),
        }
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Open<'a> {
    Comment { end: &'a str },
    String { end: &'a str, has_escape: bool },
}

#[derive(Debug, Clone)]
pub(crate) struct SyntaxState {
    updated: bool,
    open: Option<Open<'static>>,
    highlight: Vec<Highlight>,
    overlay: Option<(Range<usize>, Highlight)>,
}

impl SyntaxState {
    pub(crate) fn new() -> Self {
        SyntaxState {
            updated: false,
            open: None,
            highlight: vec![],
            overlay: None,
        }
    }

    pub(crate) fn highlight_at(&self, at: usize) -> Highlight {
        assert!(self.updated);
        if let Some((range, hl)) = &self.overlay {
            if range.contains(&at) {
                return *hl;
            }
        }
        self.highlight[at]
    }

    pub(crate) fn set_overlay(&mut self, range: Range<usize>, hl: Highlight) {
        self.overlay = Some((range, hl));
    }

    pub(crate) fn clear_overlay(&mut self) {
        self.overlay = None;
    }

    pub(crate) fn invalidate(&mut self) {
        self.updated = false;
    }

    pub(crate) fn update(
        &mut self,
        render: &str,
        syntax: &'static Syntax,
        prev: Option<&mut SyntaxState>,
        next: Option<&mut SyntaxState>,
    ) {
        assert!(prev.as_ref().map(|p| p.updated).unwrap_or(true));

        if self.updated {
            return;
        }

        self.updated = true;
        self.highlight.clear();

        let mut prev_sep = true;
        let mut open = prev.and_then(|state| state.open);

        let mut chars = render;
        while !chars.is_empty() {
            let (highlight, len) = syntax.parse(chars, &mut prev_sep, &mut open);
            self.highlight.extend(iter::repeat(highlight).take(len));
            chars = &chars[len..];
        }

        let changed = self.open != open;
        self.open = open;
        if changed {
            if let Some(next) = next {
                next.invalidate();
            }
        }
    }
}

#[derive(Debug)]
pub(crate) struct SavedHighlight {
    cx: usize,
    hl: Vec<Highlight>,
}

fn is_separator(ch: char) -> bool {
    ch.is_whitespace() || ch == '\0' || ",.()+-/*=~%<>[];".contains(ch)
}
