#![allow(clippy::trivial_regex)]

use crate::LanguageType;
use once_cell::sync::Lazy;
use regex::bytes::Regex;

pub static START_SCRIPT: Lazy<Regex> =
    Lazy::new(|| Regex::new(r#"<script(?:.*type="(.*)")?.*?>"#).unwrap());
pub static END_SCRIPT: Lazy<Regex> = Lazy::new(|| Regex::new(r#"</script>"#).unwrap());

pub static START_STYLE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r#"<style(?:.*lang="(.*)")?.*?>"#).unwrap());
pub static END_STYLE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"</style>"#).unwrap());

pub static START_TEMPLATE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r#"<template(?:.*lang="(.*)")?.*?>"#).unwrap());
pub static END_TEMPLATE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"</template>"#).unwrap());

pub static STARTING_MARKDOWN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"```\S+\s"#).unwrap());
pub static ENDING_MARKDOWN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"```\s?"#).unwrap());

/// A memory of a regex matched.
/// The values provided by `Self::start` and `Self::end` are in the same space as the
/// start value supplied to `RegexCache::build`
pub struct Capture<'a> {
    start: usize,
    text: &'a [u8],
}

impl Capture<'_> {
    #[inline(always)]
    fn start(&self) -> usize {
        self.start
    }
    #[inline(always)]
    pub fn end(&self) -> usize {
        self.start + self.text.len()
    }
    #[inline(always)]
    pub fn as_bytes(&self) -> &[u8] {
        self.text
    }
}

impl<'a> std::fmt::Debug for Capture<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Capture")
            .field("start", &self.start)
            .field("end", &self.end())
            .field("text", &String::from_utf8_lossy(self.text))
            .finish()
    }
}

pub(crate) struct RegexCache<'a> {
    inner: Option<RegexFamily<'a>>,
}

/// Embedding regexes are similar between different sets of languages.
/// `RegexFamily` records both which family the language belongs to,
/// as well as the actual matches
pub(crate) enum RegexFamily<'a> {
    HtmlLike(HtmlLike<'a>),
    Markdown(Markdown<'a>),
    Rust,
}

pub(crate) struct HtmlLike<'a> {
    start_script: Option<Box<[Capture<'a>]>>,
    start_style: Option<Box<[Capture<'a>]>>,
    start_template: Option<Box<[Capture<'a>]>>,
}

pub(crate) struct Markdown<'a> {
    starts: Option<Box<[Capture<'a>]>>,
}

impl<'a> HtmlLike<'a> {
    pub fn start_script_in_range(
        &'a self,
        start: usize,
        end: usize,
    ) -> Option<impl Iterator<Item = &'a Capture<'a>>> {
        filter_range(self.start_script.as_ref()?, start, end)
    }

    pub fn start_style_in_range(
        &'a self,
        start: usize,
        end: usize,
    ) -> Option<impl Iterator<Item = &'a Capture<'a>>> {
        filter_range(self.start_style.as_ref()?, start, end)
    }

    pub fn start_template_in_range(
        &'a self,
        start: usize,
        end: usize,
    ) -> Option<impl Iterator<Item = &'a Capture<'a>>> {
        filter_range(self.start_template.as_ref()?, start, end)
    }
}

impl<'a> Markdown<'a> {
    pub fn starts_in_range(&'a self, start: usize, end: usize) -> Option<&Capture<'a>> {
        filter_range(self.starts.as_ref()?, start, end).and_then(|mut it| it.next())
    }
}

fn filter_range<'a>(
    dataset: &'a [Capture<'a>],
    start: usize,
    end: usize,
) -> Option<impl Iterator<Item = &'a Capture<'a>>> {
    let pos = dataset
        .binary_search_by_key(&start, |cap| cap.start())
        .ok()?;

    if pos >= dataset.len() || dataset[pos].end() > end {
        None
    } else {
        Some(
            dataset[pos..]
                .iter()
                .take_while(move |cap| cap.end() <= end),
        )
    }
}

impl<'a> RegexCache<'a> {
    /// Returns the language family for which regexes were matched, if any
    pub(crate) fn family(&self) -> Option<&RegexFamily> {
        self.inner.as_ref()
    }

    /// Tries to memoize any matches of embedding regexes that occur within lines[start..end]
    /// for the given language. Any `Capture` values eventually recovered will use the same
    /// zero for their start as the given `start` argument.
    pub(crate) fn build(lang: LanguageType, lines: &'a [u8], start: usize, end: usize) -> Self {
        let inner = match lang {
            LanguageType::Markdown | LanguageType::UnrealDeveloperMarkdown => {
                let markdown = Markdown {
                    starts: save_captures(&STARTING_MARKDOWN_REGEX, lines, start, end),
                };

                if markdown.starts.is_some() {
                    Some(RegexFamily::Markdown(markdown))
                } else {
                    None
                }
            }
            LanguageType::Rust => Some(RegexFamily::Rust),
            LanguageType::Html
            | LanguageType::RubyHtml
            | LanguageType::Svelte
            | LanguageType::Vue => {
                let html = HtmlLike {
                    start_script: save_captures(&START_SCRIPT, lines, start, end),
                    start_style: save_captures(&START_STYLE, lines, start, end),
                    start_template: save_captures(&START_TEMPLATE, lines, start, end),
                };

                if html.start_script.is_some()
                    || html.start_style.is_some()
                    || html.start_template.is_some()
                {
                    Some(RegexFamily::HtmlLike(html))
                } else {
                    None
                }
            }
            _ => None,
        };
        Self { inner }
    }
}

fn save_captures<'a>(
    regex: &Regex,
    lines: &'a [u8],
    start: usize,
    end: usize,
) -> Option<Box<[Capture<'a>]>> {
    let v: Vec<_> = regex
        .captures(&lines[start..end])?
        .iter()
        .flatten()
        .map(|cap| Capture {
            start: start + cap.start(),
            text: cap.as_bytes(),
        })
        .collect();

    if v.is_empty() {
        None
    } else {
        Some(v.into())
    }
}
