#[cfg(test)]
mod test;

use std::{
    borrow::Cow,
    fmt::{Debug, Display},
};

use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use chumsky::{
    combinator::Repeated,
    error::EmptyErr,
    extra,
    input::InputRef,
    primitive::{any, choice, custom, empty, end, just, one_of, OneOf},
    recursive::recursive,
    text::{int, newline},
    IterParser, Parser,
};
use either::Either::{self, Left, Right};
use itertools::Itertools;

fn just_case_insensitive<'src>(
    s: &'static str,
) -> impl Parser<'src, &'src str, (), extra::Default> + Copy {
    any()
        .repeated()
        .exactly(s.chars().count())
        .to_slice()
        .filter(move |g: &&str| g.to_lowercase() == s)
        .ignored()
}

use crate::{
    ast::CurlyKind, ContextWorker, ContextualizationError, Contextualizer, Escaper, Value,
};

#[derive(Debug, Clone, PartialEq, Eq)]
enum ContextualizerState {
    Html(HtmlState),
    Css(CssState),
    Js(JsState),
    PlainText,
    Unsafe,
}

impl Display for ContextualizerState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ContextualizerState::Html(state) => write!(f, "html{state}"),
            ContextualizerState::Css(state) => write!(f, "css{state}"),
            ContextualizerState::Js(state) => write!(f, "js{state}"),
            ContextualizerState::PlainText => f.write_str("text"),
            ContextualizerState::Unsafe => f.write_str("unsafe"),
        }
    }
}

impl ContextualizerState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, ContextualizerState> + Clone {
        recursive(|root_name_parser| {
            choice((
                just("html")
                    .ignore_then(HtmlState::name_parser(root_name_parser.clone()))
                    .map(ContextualizerState::Html),
                just("css")
                    .ignore_then(CssState::name_parser())
                    .map(ContextualizerState::Css),
                just("js")
                    .ignore_then(JsState::name_parser())
                    .map(ContextualizerState::Js),
                just("text").to(ContextualizerState::PlainText),
                just("unsafe").to(ContextualizerState::Unsafe),
            ))
        })
    }
}

impl TryFrom<&str> for ContextualizerState {
    type Error = ();

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        ContextualizerState::name_parser()
            .parse(value)
            .into_result()
            .map_err(|_| ())
    }
}

impl ContextualizerState {
    fn parser<'src>(&self) -> impl Parser<'src, &'src str, ContextualizerState> {
        match self {
            Self::Html(state) => state.parser().map(ContextualizerState::Html).boxed(),
            Self::Css(state) => state.parser().map(ContextualizerState::Css).boxed(),
            Self::Js(state) => state.parser().map(ContextualizerState::Js).boxed(),
            Self::PlainText => any().repeated().to(ContextualizerState::PlainText).boxed(),
            Self::Unsafe => any().repeated().to(ContextualizerState::Unsafe).boxed(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum CssState {
    Regular,
    String(DelimiterKind),
    MultiLineComment,
    LineComment,
}

impl HasEmptyStategy for CssState {
    fn empty_strategy(&self) -> EmptyStrategy {
        // not even the bare string is truly bare requiring a closing brace
        EmptyStrategy::None
    }
}

impl HasMultiLineStrategy for CssState {
    fn multi_line_strategy(&self) -> MultiLineStrategy {
        match self {
            Self::LineComment | Self::String(_) => MultiLineStrategy::Condense,
            _ => MultiLineStrategy::None,
        }
    }
}

impl Display for CssState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Regular => Ok(()),
            Self::String(delim) => write!(f, ".string.{delim}"),
            Self::MultiLineComment => write!(f, ".multiline_comment"),
            Self::LineComment => write!(f, ".line_comment"),
        }
    }
}

impl NameParser for CssState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, CssState> + Clone {
        custom(|inp| {
            let savepoint = inp.save();
            inp.rewind(savepoint);
            Ok(())
        })
        .ignore_then(choice((
            just(".string.")
                .ignore_then(DelimiterKind::name_parser())
                .map(CssState::String),
            just(".multiline_comment").to(CssState::MultiLineComment),
            just(".line_comment").to(CssState::LineComment),
            empty().to(CssState::Regular),
        )))
    }
}

impl CssState {
    fn parser<'src>(&self) -> impl Parser<'src, &'src str, CssState> {
        match self {
            Self::Regular => custom(|inp| parse_css(inp).map_err(|_| EmptyErr::default())).boxed(),
            Self::String(delimiter) => {
                let delim = *delimiter;
                custom(move |inp| {
                    if parse_css_string(
                        inp,
                        one_of(match delim {
                            // referenced https://www.w3.org/TR/css-syntax-3/#consume-a-url-token; even
                            // though this appears to conflict with the go implementation
                            DelimiterKind::Bare => ")",
                            DelimiterKind::Double => "\"",
                            DelimiterKind::Single => "'",
                        })
                        .ignored(),
                    ) {
                        Ok(CssState::String(delim))
                    } else {
                        parse_css(inp).map_err(|_| EmptyErr::default())
                    }
                })
                .boxed()
            }
            Self::MultiLineComment => custom(|inp| {
                if parse_css_comment(inp, just("*/").ignored()) {
                    Ok(CssState::MultiLineComment)
                } else {
                    parse_css(inp).map_err(|_| EmptyErr::default())
                }
            })
            .boxed(),
            Self::LineComment => custom(|inp| {
                if parse_css_comment(inp, one_of("\n\x0C\r").ignored()) {
                    Ok(CssState::LineComment)
                } else {
                    parse_css(inp).map_err(|_| EmptyErr::default())
                }
            })
            .boxed(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum HtmlState {
    Regular,
    Tag(TagState<NoopInnerState>),
    ScriptTag(TagState<ScriptState>),
    StyleTag(TagState<CssState>),
    TitleTag(TagState<NoopInnerState>),
    TextAreaTag(TagState<NoopInnerState>),
    Comment,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum ScriptState {
    Js(JsState),
    UnknownScriptState(UnknownScriptState),
}

impl HasEmptyStategy for ScriptState {
    fn empty_strategy(&self) -> EmptyStrategy {
        match self {
            Self::Js(js) => js.empty_strategy(),
            Self::UnknownScriptState(_) => EmptyStrategy::None,
        }
    }
}

impl HasMultiLineStrategy for ScriptState {
    fn multi_line_strategy(&self) -> MultiLineStrategy {
        match self {
            Self::Js(js) => js.multi_line_strategy(),
            Self::UnknownScriptState(_) => MultiLineStrategy::None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct UnknownScriptState;

impl NameParser for UnknownScriptState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone {
        empty().to(Self)
    }
}

impl NameParser for ScriptState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone {
        JsState::name_parser()
            .map(ScriptState::Js)
            .or(empty().to(ScriptState::UnknownScriptState(UnknownScriptState)))
    }
}

impl Display for ScriptState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnknownScriptState(UnknownScriptState) => Ok(()),
            Self::Js(js) => Display::fmt(js, f),
        }
    }
}

impl ScriptState {
    fn parser<'src>(&self) -> impl Parser<'src, &'src str, ScriptState> {
        match self {
            Self::UnknownScriptState(UnknownScriptState) => any()
                .repeated()
                .to(ScriptState::UnknownScriptState(UnknownScriptState))
                .boxed(),
            Self::Js(js) => js.parser().map(ScriptState::Js).boxed(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct NoopInnerState;

impl HasEmptyStategy for NoopInnerState {
    fn empty_strategy(&self) -> EmptyStrategy {
        EmptyStrategy::None
    }
}

impl HasMultiLineStrategy for NoopInnerState {
    fn multi_line_strategy(&self) -> MultiLineStrategy {
        MultiLineStrategy::None
    }
}

impl NameParser for NoopInnerState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone {
        any()
            .filter(|_| false)
            .map(|_| unreachable!("a noop inner state can never be parsed"))
    }
}

impl Display for NoopInnerState {
    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Ok(())
    }
}

impl Display for HtmlState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Regular => Ok(()),
            Self::Tag(state) => write!(f, ".tag{}", state.sub_state),
            Self::ScriptTag(
                state @ TagState {
                    type_attr_value_is_js_or_unspecified: true,
                    ..
                },
            ) => {
                write!(f, ".js_script_tag{}", state.sub_state)
            }
            Self::ScriptTag(state) => write!(f, ".unknown_script_tag{}", state.sub_state),
            Self::StyleTag(state) => write!(f, ".style_tag{}", state.sub_state),
            Self::TitleTag(state) => write!(f, ".title_tag{}", state.sub_state),
            Self::TextAreaTag(state) => write!(f, ".text_area_tag{}", state.sub_state),
            Self::Comment => f.write_str(".comment"),
        }
    }
}

/// returns the content of the html special tag and whether input ended within in; assumes the start
/// tag was already completly parsed. `end_text` is expected to be of the format "</tag" where tag is
/// a stand in for "script", "style", ...
fn parse_html_special_tag_contents<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
    end_text: &'static str,
) -> Result<(&'s str, bool), EmptyErr> {
    let tag_end = just_case_insensitive(end_text);
    inp.parse(
        just("\\<")
            .ignored()
            .or(any().and_is(tag_end.not()).ignored())
            .repeated()
            .to_slice()
            .then(
                tag_end
                    .then(html_whitespace())
                    .then(just('>'))
                    .to(false)
                    .or(end().to(true)),
            ),
    )
}

impl HtmlState {
    fn name_parser<'src>(
        root_name_parser: impl Parser<'src, &'src str, ContextualizerState> + Clone,
    ) -> impl Parser<'src, &'src str, HtmlState> + Clone {
        choice((
            just(".tag")
                .ignore_then(TagState::<NoopInnerState>::name_parser(
                    root_name_parser.clone(),
                ))
                .map(HtmlState::Tag),
            just(".js_script_tag")
                .ignore_then(TagState::<JsState>::name_parser(root_name_parser.clone()))
                .map(
                    |TagState {
                         type_attr_value_is_js_or_unspecified: _,
                         sub_state,
                     }| {
                        HtmlState::ScriptTag(TagState {
                            sub_state: match sub_state {
                                TagSubState::Attributes => TagSubState::Attributes,
                                TagSubState::AttributeName => TagSubState::AttributeName,
                                TagSubState::AttributeValue(inner, delim) => {
                                    TagSubState::AttributeValue(inner, delim)
                                }
                                TagSubState::Inner(inner) => {
                                    TagSubState::Inner(ScriptState::Js(inner))
                                }
                            },
                            type_attr_value_is_js_or_unspecified: true,
                        })
                    },
                ),
            just(".unknown_script_tag")
                .ignore_then(TagState::<UnknownScriptState>::name_parser(
                    root_name_parser.clone(),
                ))
                .map(
                    |TagState {
                         type_attr_value_is_js_or_unspecified: _,
                         sub_state,
                     }| {
                        HtmlState::ScriptTag(TagState {
                            type_attr_value_is_js_or_unspecified: false,
                            sub_state: match sub_state {
                                TagSubState::Inner(UnknownScriptState) => TagSubState::Inner(
                                    ScriptState::UnknownScriptState(UnknownScriptState),
                                ),
                                TagSubState::Attributes => TagSubState::Attributes,
                                TagSubState::AttributeValue(inner, delim) => {
                                    TagSubState::AttributeValue(inner, delim)
                                }
                                TagSubState::AttributeName => TagSubState::AttributeName,
                            },
                        })
                    },
                ),
            just(".style_tag")
                .ignore_then(TagState::<CssState>::name_parser(root_name_parser.clone()))
                .map(HtmlState::StyleTag),
            just(".title_tag")
                .ignore_then(TagState::<NoopInnerState>::name_parser(
                    root_name_parser.clone(),
                ))
                .map(HtmlState::TitleTag),
            just(".text_area_tag")
                .ignore_then(TagState::<NoopInnerState>::name_parser(
                    root_name_parser.clone(),
                ))
                .map(HtmlState::TextAreaTag),
            just(".comment").to(HtmlState::Comment),
            empty().to(HtmlState::Regular),
        ))
    }

    #[allow(clippy::too_many_lines)]
    fn parser<'src>(&self) -> impl Parser<'src, &'src str, HtmlState> {
        fn special_parser<'src, T>(
            end_text: &'static str,
            wrapper: impl Fn(TagState<T>) -> HtmlState,
            inner: impl Fn(&'src str) -> Result<T, EmptyErr>,
            type_attr_value_is_js_or_unspecified: bool,
        ) -> impl Parser<'src, &'src str, HtmlState, extra::Default> {
            custom(move |inp| {
                let (content, ended_within) = parse_html_special_tag_contents(inp, end_text)?;

                if ended_within {
                    return Ok(wrapper(TagState {
                        type_attr_value_is_js_or_unspecified,
                        sub_state: TagSubState::Inner(inner(content)?),
                    }));
                }

                parse_html(inp).map_err(|_| EmptyErr::default())
            })
        }

        match self {
            Self::Regular => custom(|inp| parse_html(inp).map_err(|_| EmptyErr::default())).boxed(),
            Self::Tag(state) => {
                let state = state.clone();
                custom(
                    move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
                        TagStateParseRes::Inner(_) | TagStateParseRes::SelfClosing => {
                            parse_html(inp).map_err(|_| EmptyErr::default())
                        }
                        TagStateParseRes::State(state) => Ok(HtmlState::Tag(state.into())),
                    },
                )
                .boxed()
            }
            Self::ScriptTag(TagState {
                type_attr_value_is_js_or_unspecified,
                sub_state: TagSubState::Inner(state),
            }) => {
                let state = state.clone();
                special_parser(
                    "</script",
                    HtmlState::ScriptTag,
                    move |content| {
                        state
                            .parser()
                            .parse(content)
                            .into_result()
                            .map_err(|_| EmptyErr::default())
                    },
                    *type_attr_value_is_js_or_unspecified,
                )
                .boxed()
            }
            Self::ScriptTag(state) => {
                let state = state.clone();
                custom(
                    move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
                        TagStateParseRes::Inner(type_attr_value_is_js_or_unspecified) => {
                            inp.parse(special_parser(
                                "</script",
                                HtmlState::ScriptTag,
                                |content| {
                                    if type_attr_value_is_js_or_unspecified {
                                        custom(|inp| Ok(ScriptState::Js(parse_js(inp, vec![]))))
                                            .parse(content)
                                            .into_result()
                                            .map_err(|_| EmptyErr::default())
                                    } else {
                                        Ok(ScriptState::UnknownScriptState(UnknownScriptState))
                                    }
                                },
                                type_attr_value_is_js_or_unspecified,
                            ))
                        }
                        TagStateParseRes::SelfClosing => {
                            parse_html(inp).map_err(|_| EmptyErr::default())
                        }
                        TagStateParseRes::State(state) => Ok(HtmlState::ScriptTag(state.into())),
                    },
                )
                .boxed()
            }
            Self::StyleTag(TagState {
                type_attr_value_is_js_or_unspecified: _,
                sub_state: TagSubState::Inner(state),
            }) => {
                let state = state.clone();
                special_parser(
                    "</style",
                    HtmlState::StyleTag,
                    move |content| {
                        state
                            .parser()
                            .parse(content)
                            .into_result()
                            .map_err(|_| EmptyErr::default())
                    },
                    true,
                )
                .boxed()
            }
            Self::StyleTag(state) => {
                let state = state.clone();
                custom(
                    move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
                        TagStateParseRes::Inner(_type_attr_value) => inp.parse(special_parser(
                            "</style",
                            HtmlState::StyleTag,
                            |content| {
                                custom(|inp| parse_css(inp).map_err(|_| EmptyErr::default()))
                                    .parse(content)
                                    .into_result()
                                    .map_err(|_| EmptyErr::default())
                            },
                            true,
                        )),
                        TagStateParseRes::SelfClosing => {
                            parse_html(inp).map_err(|_| EmptyErr::default())
                        }
                        TagStateParseRes::State(state) => Ok(HtmlState::StyleTag(state.into())),
                    },
                )
                .boxed()
            }
            Self::TitleTag(state) => {
                let state = state.clone();
                custom(
                    move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
                        TagStateParseRes::Inner(_type_attr_value) => inp.parse(special_parser(
                            "</title",
                            HtmlState::TitleTag,
                            |_content| Ok(NoopInnerState),
                            state.type_attr_value_is_js_or_unspecified,
                        )),
                        TagStateParseRes::SelfClosing => {
                            parse_html(inp).map_err(|_| EmptyErr::default())
                        }
                        TagStateParseRes::State(state) => Ok(HtmlState::TitleTag(state.into())),
                    },
                )
                .boxed()
            }
            Self::TextAreaTag(state) => {
                let state = state.clone();
                custom(
                    move |inp| match state.parse(inp).map_err(|_| EmptyErr::default())? {
                        TagStateParseRes::Inner(_type_attr_value) => inp.parse(special_parser(
                            "</textarea",
                            HtmlState::TextAreaTag,
                            |_content| Ok(NoopInnerState),
                            true,
                        )),
                        TagStateParseRes::SelfClosing => {
                            parse_html(inp).map_err(|_| EmptyErr::default())
                        }
                        TagStateParseRes::State(state) => Ok(HtmlState::TextAreaTag(state.into())),
                    },
                )
                .boxed()
            }
            Self::Comment => custom(|inp| {
                if parse_html_comment(inp) {
                    Ok(HtmlState::Comment)
                } else {
                    parse_html(inp).map_err(|_| EmptyErr::default())
                }
            })
            .boxed(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct TagState<T> {
    type_attr_value_is_js_or_unspecified: bool,
    sub_state: TagSubState<T>,
}

impl<T: HasEmptyStategy> HasEmptyStategy for TagState<T> {
    fn empty_strategy(&self) -> EmptyStrategy {
        self.sub_state.empty_strategy()
    }
}

impl<T: HasMultiLineStrategy> HasMultiLineStrategy for TagState<T> {
    fn multi_line_strategy(&self) -> MultiLineStrategy {
        self.sub_state.multi_line_strategy()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct TagStateWithoutInner {
    type_attr_value_is_js_or_unspecified: bool,
    sub_state: TagSubStateWithoutInner,
}

impl<T> From<TagStateWithoutInner> for TagState<T> {
    fn from(
        TagStateWithoutInner {
            type_attr_value_is_js_or_unspecified,
            sub_state,
        }: TagStateWithoutInner,
    ) -> Self {
        Self {
            type_attr_value_is_js_or_unspecified,
            sub_state: sub_state.into(),
        }
    }
}

enum TagStateParseRes {
    State(TagStateWithoutInner),
    SelfClosing,
    Inner(bool),
}

impl<T: NameParser + Clone> TagState<T> {
    fn name_parser<'src>(
        root_name_parser: impl Parser<'src, &'src str, ContextualizerState> + Clone,
    ) -> impl Parser<'src, &'src str, TagState<T>> + Clone {
        TagSubState::name_parser(root_name_parser.clone()).map(|sub_state| TagState {
            type_attr_value_is_js_or_unspecified: true,
            sub_state,
        })
    }

    fn parse<'src>(
        &self,
        inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
    ) -> Result<TagStateParseRes, ContextualizationError> {
        match &self.sub_state {
            TagSubState::AttributeName => {
                let (_additional, end) = inp
                    .parse(parse_tag_attr_name(false))
                    .map_err(|_| ContextualizationError::InvalidStatic)?;
                if end {
                    return Ok(TagStateParseRes::State(TagStateWithoutInner {
                        type_attr_value_is_js_or_unspecified: self
                            .type_attr_value_is_js_or_unspecified,
                        sub_state: TagSubStateWithoutInner::AttributeName,
                    }));
                }
                match parse_tag_attr_after_name(inp, None)? {
                    TagAttrRes::None => {
                        match parse_tag_inner(inp, self.type_attr_value_is_js_or_unspecified)? {
                            Left(state) => Ok(TagStateParseRes::State(state)),
                            Right((type_attr_value, false)) => {
                                Ok(TagStateParseRes::Inner(type_attr_value))
                            }
                            Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
                        }
                    }
                    TagAttrRes::End(sub_state) => {
                        Ok(TagStateParseRes::State(TagStateWithoutInner {
                            type_attr_value_is_js_or_unspecified: self
                                .type_attr_value_is_js_or_unspecified,
                            sub_state,
                        }))
                    }
                    TagAttrRes::NewTypeAttrValueIsJs(value) => match parse_tag_inner(inp, value)? {
                        Left(state) => Ok(TagStateParseRes::State(state)),
                        Right((type_attr_value, false)) => {
                            Ok(TagStateParseRes::Inner(type_attr_value))
                        }
                        Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
                    },
                }
            }
            TagSubState::Attributes => {
                match parse_tag_inner(inp, self.type_attr_value_is_js_or_unspecified)? {
                    Left(state) => Ok(TagStateParseRes::State(state)),
                    Right((type_attr_value_is_js_or_unspecified, false)) => Ok(
                        TagStateParseRes::Inner(type_attr_value_is_js_or_unspecified),
                    ),
                    Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
                }
            }
            TagSubState::AttributeValue(ref inner_state, delim) => {
                let parser = inner_state.parser();
                let s = match delim {
                    DelimiterKind::Bare => inp.parse(attr_value_bare_parser()),
                    DelimiterKind::Double => inp.parse(attr_value_double_parser()),
                    DelimiterKind::Single => inp.parse(attr_value_single_parser()),
                }
                .expect(REPEATED_LOWER_BOUND_FAIL);

                if inp
                    .parse(
                        end().to(true).or(match delim {
                            DelimiterKind::Bare => empty().boxed(),
                            DelimiterKind::Double => just('"').ignored().boxed(),
                            DelimiterKind::Single => just('\'').ignored().boxed(),
                        }
                        .to(false)),
                    )
                    .map_err(|_| ContextualizationError::InvalidStatic)?
                {
                    let state = parser
                        .parse(s)
                        .into_result()
                        .map_err(|_| ContextualizationError::InvalidStatic)?;

                    return Ok(TagStateParseRes::State(TagStateWithoutInner {
                        type_attr_value_is_js_or_unspecified: self
                            .type_attr_value_is_js_or_unspecified,
                        sub_state: TagSubStateWithoutInner::AttributeValue(Box::new(state), *delim),
                    }));
                }

                match parse_tag_inner(inp, self.type_attr_value_is_js_or_unspecified)? {
                    Left(state) => Ok(TagStateParseRes::State(state)),
                    Right((type_attr_value, false)) => Ok(TagStateParseRes::Inner(type_attr_value)),
                    Right((_type_attr_value, true)) => Ok(TagStateParseRes::SelfClosing),
                }
            }
            TagSubState::Inner(_t) => Ok(TagStateParseRes::Inner(true)),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum TagSubState<T> {
    Attributes,
    AttributeName,
    AttributeValue(Box<ContextualizerState>, DelimiterKind),
    Inner(T),
}

impl<T: HasEmptyStategy> HasEmptyStategy for TagSubState<T> {
    fn empty_strategy(&self) -> EmptyStrategy {
        match self {
            Self::AttributeValue(inner, DelimiterKind::Bare) => {
                inner.empty_strategy().max(EmptyStrategy::Deny)
            }
            Self::AttributeValue(inner, _) => inner.empty_strategy(),
            Self::Inner(inner) => inner.empty_strategy(),
            _ => EmptyStrategy::None,
        }
    }
}

impl<T: HasMultiLineStrategy> HasMultiLineStrategy for TagSubState<T> {
    fn multi_line_strategy(&self) -> MultiLineStrategy {
        match self {
            Self::AttributeValue(_inner, DelimiterKind::Bare) => MultiLineStrategy::Condense,
            Self::AttributeValue(inner, _) => inner.multi_line_strategy(),
            Self::AttributeName => MultiLineStrategy::Condense,
            Self::Attributes => MultiLineStrategy::None,
            Self::Inner(inner) => inner.multi_line_strategy(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum TagSubStateWithoutInner {
    Attributes,
    AttributeName,
    AttributeValue(Box<ContextualizerState>, DelimiterKind),
}

impl<T> From<TagSubStateWithoutInner> for TagSubState<T> {
    fn from(value: TagSubStateWithoutInner) -> Self {
        match value {
            TagSubStateWithoutInner::Attributes => Self::Attributes,
            TagSubStateWithoutInner::AttributeName => Self::AttributeName,
            TagSubStateWithoutInner::AttributeValue(inner, delim) => {
                Self::AttributeValue(inner, delim)
            }
        }
    }
}

impl<T: Display> Display for TagSubState<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Attributes => f.write_str(".attrs"),
            Self::AttributeName => f.write_str(".attr_name"),
            Self::AttributeValue(inner, delim) => write!(f, ".attr_value.{delim}.{inner}"),
            Self::Inner(t) => t.fmt(f),
        }
    }
}

trait NameParser: Sized {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, Self> + Clone;
}

impl<T: NameParser + Clone> TagSubState<T> {
    fn name_parser<'src>(
        root_name_parser: impl Parser<'src, &'src str, ContextualizerState> + Clone,
    ) -> impl Parser<'src, &'src str, TagSubState<T>> + Clone {
        choice((
            just(".attrs").to(TagSubState::Attributes),
            just(".attr_name").to(TagSubState::AttributeName),
            just(".attr_value.")
                .ignore_then(DelimiterKind::name_parser())
                .then_ignore(just('.'))
                .then(root_name_parser)
                .map(|(delim, state)| TagSubState::AttributeValue(Box::new(state), delim)),
            T::name_parser().map(TagSubState::Inner),
        ))
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct JsState {
    tmpl_literal_brace_counts: Vec<usize>,
    sub_state: JsSubState,
}

impl HasEmptyStategy for JsState {
    fn empty_strategy(&self) -> EmptyStrategy {
        match self.sub_state {
            JsSubState::Regex { in_charset: false } => EmptyStrategy::ReplaceRegex,
            _ => EmptyStrategy::None,
        }
    }
}

impl HasMultiLineStrategy for JsState {
    fn multi_line_strategy(&self) -> MultiLineStrategy {
        match self.sub_state {
            JsSubState::Regex { in_charset: _ }
            | JsSubState::LineComment
            | JsSubState::StringDouble
            | JsSubState::StringSingle => MultiLineStrategy::Condense,
            _ => MultiLineStrategy::None,
        }
    }
}

impl Display for JsState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.tmpl_literal_brace_counts.is_empty() {
            Display::fmt(&self.sub_state, f)
        } else {
            let braces = self.tmpl_literal_brace_counts.iter().join(".");
            write!(f, ".{braces}{}", self.sub_state)
        }
    }
}

impl NameParser for JsState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, JsState> + Clone {
        just(".")
            .ignore_then(
                int(10)
                    .try_map(|int: &'src str, _span| {
                        int.parse::<usize>()
                            .map_err(|_| EmptyErr::default())
                            .and_then(|i| (i != 0).then_some(i).ok_or(EmptyErr::default()))
                    })
                    .separated_by(just("."))
                    .at_least(1)
                    .collect::<Vec<usize>>(),
            )
            .or(empty().to(vec![]))
            .then(JsSubState::name_parser())
            .map(|(tmpl_literal_brace_counts, sub_state)| JsState {
                tmpl_literal_brace_counts,
                sub_state,
            })
    }
}

impl JsState {
    fn parser<'src>(&self) -> impl Parser<'src, &'src str, JsState> {
        let braces = self.tmpl_literal_brace_counts.clone();
        match self.sub_state {
            JsSubState::Regular => custom(move |inp| Ok(parse_js(inp, braces.clone()))).boxed(),
            JsSubState::StringDouble => custom(move |inp| {
                Ok(if inp.parse(js_string_parser('"'))? {
                    JsState {
                        tmpl_literal_brace_counts: braces.clone(),
                        sub_state: JsSubState::StringDouble,
                    }
                } else {
                    parse_js(inp, braces.clone())
                })
            })
            .boxed(),
            JsSubState::StringSingle => custom(move |inp| {
                Ok(if inp.parse(js_string_parser('\''))? {
                    JsState {
                        tmpl_literal_brace_counts: braces.clone(),
                        sub_state: JsSubState::StringSingle,
                    }
                } else {
                    parse_js(inp, braces.clone())
                })
            })
            .boxed(),
            JsSubState::Regex { in_charset } => custom(move |inp| {
                Ok(
                    if let Some(state) = parse_js_regex(inp, braces.clone(), in_charset) {
                        state
                    } else {
                        parse_js(inp, braces.clone())
                    },
                )
            })
            .boxed(),
            JsSubState::TmplLit(state) => custom(move |inp| {
                Ok(match parse_js_tmpl_literal(inp, state) {
                    JsTmplLiteralItem::End(state) => JsState {
                        tmpl_literal_brace_counts: braces.clone(),
                        sub_state: JsSubState::TmplLit(state),
                    },
                    JsTmplLiteralItem::ExpressionStart => {
                        let mut new_braces = braces.clone();
                        new_braces.push(1);
                        parse_js(inp, new_braces)
                    }
                    JsTmplLiteralItem::LiteralEnd => parse_js(inp, braces.clone()),
                })
            })
            .boxed(),
            JsSubState::LineComment => custom(move |inp| {
                Ok(
                    if let Some(state) = parse_js_line_comment(inp, braces.clone()) {
                        state
                    } else {
                        parse_js(inp, braces.clone())
                    },
                )
            })
            .boxed(),
            JsSubState::MultiLineComment => custom(move |inp| {
                Ok(
                    if let Some(state) = parse_js_multi_line_comment(inp, braces.clone()) {
                        state
                    } else {
                        parse_js(inp, braces.clone())
                    },
                )
            })
            .boxed(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum JsSubState {
    Regular,
    StringDouble,
    StringSingle,
    Regex { in_charset: bool },
    TmplLit(TmplLitState),
    LineComment,
    MultiLineComment,
}

impl Display for JsSubState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Regular => Ok(()),
            Self::StringDouble => f.write_str(".string.d"),
            Self::StringSingle => f.write_str(".string.s"),
            Self::Regex { in_charset: true } => f.write_str(".regex.charset"),
            Self::Regex { in_charset: false } => f.write_str(".regex"),
            Self::TmplLit(inner) => write!(f, ".tmpl_lit{inner}"),
            Self::LineComment => f.write_str(".line_comment"),
            Self::MultiLineComment => f.write_str(".multiline_comment"),
        }
    }
}

impl JsSubState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, JsSubState> + Clone {
        choice((
            just(".string.d").to(JsSubState::StringDouble),
            just(".string.s").to(JsSubState::StringSingle),
            just(".regex.charset").to(JsSubState::Regex { in_charset: true }),
            just(".regex").to(JsSubState::Regex { in_charset: false }),
            just(".tmpl_lit")
                .ignore_then(TmplLitState::name_parser())
                .map(JsSubState::TmplLit),
            just(".line_comment").to(JsSubState::LineComment),
            just(".multiline_comment").to(JsSubState::MultiLineComment),
            empty().to(JsSubState::Regular),
        ))
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TmplLitState {
    Regular,
    DollarSign,
}

impl Display for TmplLitState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Regular => Ok(()),
            Self::DollarSign => f.write_str(".dollar_sign"),
        }
    }
}

impl TmplLitState {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, TmplLitState> + Clone {
        choice((
            just(".dollar_sign").to(TmplLitState::DollarSign),
            empty().to(TmplLitState::Regular),
        ))
    }
}

const REPEATED_LOWER_BOUND_FAIL: &str = "repeated without lower bound cannot fail";
const INCLUDED_END_FAIL: &str = "cannot fail since end is included";

fn html_whitespace<'src>(
) -> Repeated<OneOf<&'static str, &'src str, extra::Default>, char, &'src str, extra::Default> {
    one_of(" \t\n\x0C\r").repeated()
}

// todo: shorten this function
#[allow(clippy::too_many_lines)]
fn parse_html<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
) -> Result<HtmlState, ContextualizationError> {
    #[derive(Debug, Clone, Copy)]
    enum TagOpeningKind {
        Comment,
        Closing,
        Script,
        Style,
        TextArea,
        Title,
        Regular,
    }
    let tag_start = just('<');

    let tag = any()
        .filter(|c: &char| c.is_ascii_alphabetic())
        .then(
            any()
                .filter(|c: &char| c.is_ascii_alphanumeric())
                .repeated()
                .at_least(1)
                .separated_by(one_of(":-")),
        )
        .to_slice()
        .then_ignore(end().not());

    loop {
        let Some(tag) = inp
            .parse(
                any()
                    .ignored()
                    .and_is(tag_start.not())
                    .or(just_case_insensitive("<!doctype")
                        .then(html_whitespace().at_least(1))
                        .then(just_case_insensitive("html"))
                        .then(html_whitespace())
                        .then(just(">"))
                        .ignored())
                    .repeated()
                    .ignore_then(
                        end().to(None).or(tag_start
                            .ignore_then(choice((
                                just("!--").to(TagOpeningKind::Comment),
                                just("/")
                                    .then(tag)
                                    .then(html_whitespace())
                                    .then(just(">"))
                                    .to(TagOpeningKind::Closing),
                                tag.map(|t: &str| match t.to_lowercase().as_str() {
                                    "script" => TagOpeningKind::Script,
                                    "style" => TagOpeningKind::Style,
                                    "textarea" => TagOpeningKind::TextArea,
                                    "title" => TagOpeningKind::Title,
                                    _ => TagOpeningKind::Regular,
                                }),
                            )))
                            .map(Some)),
                    ),
            )
            .map_err(|_| ContextualizationError::InvalidStatic)?
        else {
            return Ok(HtmlState::Regular);
        };

        match tag {
            TagOpeningKind::Comment => {
                if parse_html_comment(inp) {
                    return Ok(HtmlState::Comment);
                }
            }
            TagOpeningKind::Closing => (),
            TagOpeningKind::Regular => match parse_tag_inner(inp, true)? {
                Left(state) => return Ok(HtmlState::Tag(state.into())),
                Right((_type_attr_value, _self_closing)) => (),
            },
            TagOpeningKind::TextArea => match parse_tag_inner(inp, true)? {
                Left(state) => return Ok(HtmlState::TextAreaTag(state.into())),
                Right((_type_attr_value, self_closing)) => {
                    if self_closing {
                        continue;
                    }
                    let (_content, ended_within) =
                        parse_html_special_tag_contents(inp, "</textarea")
                            .map_err(|_| ContextualizationError::InvalidStatic)?;

                    if !ended_within {
                        return Ok(HtmlState::TextAreaTag(TagState {
                            type_attr_value_is_js_or_unspecified: true,
                            sub_state: TagSubState::Inner(NoopInnerState),
                        }));
                    }
                }
            },
            TagOpeningKind::Title => match parse_tag_inner(inp, true)? {
                Left(state) => return Ok(HtmlState::TitleTag(state.into())),
                Right((_type_attr_value, self_closing)) => {
                    if self_closing {
                        continue;
                    }
                    let (_content, ended_within) = parse_html_special_tag_contents(inp, "</title")
                        .map_err(|_| ContextualizationError::InvalidStatic)?;

                    if ended_within {
                        return Ok(HtmlState::TitleTag(TagState {
                            type_attr_value_is_js_or_unspecified: true,
                            sub_state: TagSubState::Inner(NoopInnerState),
                        }));
                    }
                }
            },
            TagOpeningKind::Script => match parse_tag_inner(inp, true)? {
                Left(state) => return Ok(HtmlState::ScriptTag(state.into())),
                Right((is_js, self_closing)) => {
                    if self_closing {
                        continue;
                    }

                    let (content, ended_within) = parse_html_special_tag_contents(inp, "</script")
                        .map_err(|_| ContextualizationError::InvalidStatic)?;

                    if !ended_within {
                        continue;
                    }

                    return Ok(if is_js {
                        let state = custom(|inp| Ok(parse_js(inp, vec![])))
                            .parse(content)
                            .into_result()
                            .map_err(|_| ContextualizationError::InvalidStatic)?;

                        HtmlState::ScriptTag(TagState {
                            type_attr_value_is_js_or_unspecified: true,
                            sub_state: TagSubState::Inner(ScriptState::Js(state)),
                        })
                    } else {
                        HtmlState::ScriptTag(TagState {
                            type_attr_value_is_js_or_unspecified: false,
                            sub_state: TagSubState::Inner(ScriptState::UnknownScriptState(
                                UnknownScriptState,
                            )),
                        })
                    });
                }
            },
            TagOpeningKind::Style => match parse_tag_inner(inp, true)? {
                Left(state) => return Ok(HtmlState::StyleTag(state.into())),
                Right((_type_attr_value, self_closing)) => {
                    if self_closing {
                        continue;
                    }

                    let (content, ended_within) =
                        parse_html_special_tag_contents(inp, "</style")
                            .map_err(|_| ContextualizationError::InvalidStatic)?;

                    if !ended_within {
                        continue;
                    }

                    let state = custom(|inp| Ok(parse_css(inp)))
                        .parse(content)
                        .into_result()
                        .map_err(|_| ContextualizationError::InvalidStatic)??;

                    return Ok(HtmlState::StyleTag(TagState {
                        type_attr_value_is_js_or_unspecified: true,
                        sub_state: TagSubState::Inner(state),
                    }));
                }
            },
        }
    }
}

/// returns whether input ends in html comment
fn parse_html_comment<'s>(inp: &mut InputRef<'s, '_, &'s str, extra::Default>) -> bool {
    let comment_end = just("-->");

    inp.parse(
        any()
            .and_is(comment_end.not())
            .repeated()
            .ignore_then(comment_end.to(false).or(end().to(true))),
    )
    .expect(INCLUDED_END_FAIL)
}

fn parse_tag_attr_name<'s>(standalone: bool) -> impl Parser<'s, &'s str, (&'s str, bool)> {
    let end_chars = one_of(" \t\n\x0C\r=>");
    let illegal_chars = one_of("'\"<");
    any()
        .and_is(end_chars.or(illegal_chars).not())
        .repeated()
        .at_least(standalone.into())
        .to_slice()
        .then(end().or_not().map(|end| end.is_some()))
}

// todo: make return of type_attr_value optional since it's only relevant for the script tag but
// incurs cost on every tag
// todo: do something about this functions return type
#[allow(clippy::type_complexity)]
fn parse_tag_inner<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
    mut type_attr_value_is_js_or_unspecified: bool,
) -> Result<Either<TagStateWithoutInner, (bool, bool)>, ContextualizationError> {
    let tag_end = just('>');
    let end_chars = one_of(" \t\n\u{C}\r=>");
    let illegal_chars = one_of("'\"<");

    let self_closing = loop {
        #[derive(Debug, Clone, Copy)]
        enum TagContinuation<'s> {
            End,
            TagEnd,
            SelfClosing,
            Attribute(&'s str, bool),
            Illegal,
        }

        let cont = inp
            .parse(
                html_whitespace().ignore_then(choice((
                    end().to(TagContinuation::End),
                    tag_end.to(TagContinuation::TagEnd),
                    just('/')
                        .then(html_whitespace())
                        .then(tag_end)
                        .to(TagContinuation::SelfClosing),
                    // this was taken from the go code; todo: figure out if the default rejection
                    // of parsers of unknown input makes this redundant
                    any()
                        .and_is(end_chars.or(illegal_chars).not())
                        .repeated()
                        .then(illegal_chars)
                        .to(TagContinuation::Illegal),
                    parse_tag_attr_name(true)
                        .map(|(attr_name, end)| TagContinuation::Attribute(attr_name, end)),
                ))),
            )
            .map_err(|_| ContextualizationError::InvalidStatic)?;

        match cont {
            TagContinuation::End => {
                return Ok(Left(TagStateWithoutInner {
                    type_attr_value_is_js_or_unspecified,
                    sub_state: TagSubStateWithoutInner::Attributes,
                }))
            }
            TagContinuation::TagEnd => break false,
            TagContinuation::SelfClosing => break true,
            TagContinuation::Illegal => return Err(ContextualizationError::InvalidStatic),
            TagContinuation::Attribute(attr, end) => {
                if end {
                    return Ok(Left(TagStateWithoutInner {
                        type_attr_value_is_js_or_unspecified,
                        sub_state: TagSubStateWithoutInner::AttributeName,
                    }));
                }
                match parse_tag_attr_after_name(inp, Some(attr))? {
                    TagAttrRes::None => (),
                    TagAttrRes::NewTypeAttrValueIsJs(new_type_attr_value) => {
                        type_attr_value_is_js_or_unspecified = new_type_attr_value;
                    }
                    TagAttrRes::End(sub_state) => {
                        return Ok(Left(TagStateWithoutInner {
                            type_attr_value_is_js_or_unspecified,
                            sub_state,
                        }))
                    }
                }
            }
        }
    };

    Ok(Right((type_attr_value_is_js_or_unspecified, self_closing)))
}

enum TagAttrRes {
    None,
    NewTypeAttrValueIsJs(bool),
    End(TagSubStateWithoutInner),
}

fn parse_tag_attr_after_name<'src>(
    inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
    attr: Option<&'src str>,
) -> Result<TagAttrRes, ContextualizationError> {
    if !inp
        .parse(
            just('=')
                .padded_by(html_whitespace())
                .or_not()
                .map(|eq| eq.is_some()),
        )
        .map_err(|_| ContextualizationError::InvalidStatic)?
    {
        return Ok(TagAttrRes::None);
    }

    let attr = attr.map(str::to_lowercase);
    let attr_kind: AttributeKind = attr.as_deref().map_or(AttributeKind::Unsafe, Into::into);

    let (value, delim) = parse_attr_value(inp)?;

    let state = custom(|inp| match attr_kind {
        AttributeKind::Css => parse_css(inp)
            .map(ContextualizerState::Css)
            .map_err(|_| EmptyErr::default()),
        AttributeKind::Html => parse_html(inp)
            .map(ContextualizerState::Html)
            .map_err(|_| EmptyErr::default()),
        AttributeKind::Js => Ok(ContextualizerState::Js(parse_js(inp, vec![]))),
        AttributeKind::Plain => {
            inp.parse(any().repeated())
                .expect(REPEATED_LOWER_BOUND_FAIL);
            Ok(ContextualizerState::PlainText)
        }
        AttributeKind::Unsafe => {
            inp.parse(any().repeated())
                .expect(REPEATED_LOWER_BOUND_FAIL);
            Ok(ContextualizerState::Unsafe)
        }
    })
    .parse(&value)
    .into_result()
    .map_err(|_| ContextualizationError::InvalidStatic)?;

    if let Some(delim) = delim {
        return Ok(TagAttrRes::End(TagSubStateWithoutInner::AttributeValue(
            Box::new(state),
            delim,
        )));
    }

    Ok(if attr.is_some_and(|attr| &attr == "type") {
        TagAttrRes::NewTypeAttrValueIsJs(is_js(&value))
    } else {
        TagAttrRes::None
    })
}

// referenced https://github.com/golang/go/blob/master/src/html/template/js.go

fn js_whitespace<'src>() -> impl Parser<'src, &'src str, (), extra::Default> + Copy {
    one_of("\u{C}\n\r\t\x0B\u{0020}\u{00a0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200a}\u{2028}\u{2029}\u{202f}\u{205f}\u{3000}\u{feff}").repeated()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JsTmplLiteralItem {
    ExpressionStart,
    LiteralEnd,
    End(TmplLitState),
}

fn parse_js_tmpl_literal<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
    mut state: TmplLitState,
) -> JsTmplLiteralItem {
    loop {
        let item = choice((
            match state {
                TmplLitState::Regular => just('$'),
                TmplLitState::DollarSign => just('{'),
            }
            .to(None),
            just('`').to(JsTmplLiteralItem::LiteralEnd).map(Some),
            end().to(state).map(JsTmplLiteralItem::End).map(Some),
        ));

        match state {
            TmplLitState::Regular => {
                match inp
                    .parse(
                        just('\\')
                            .then(any())
                            .ignored()
                            .or(any().and_is(item.not()).ignored())
                            .repeated()
                            .ignore_then(item),
                    )
                    .expect(INCLUDED_END_FAIL)
                {
                    Some(item) => return item,
                    None => state = TmplLitState::DollarSign,
                }
            }
            TmplLitState::DollarSign => match inp.parse(item.or_not()).expect(INCLUDED_END_FAIL) {
                Some(Some(item)) => return item,
                Some(None) => return JsTmplLiteralItem::ExpressionStart,
                None => state = TmplLitState::Regular,
            },
        }
    }
}

#[allow(clippy::too_many_lines)]
fn parse_js<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
    mut tmpl_literal_brace_counts: Vec<usize>,
) -> JsState {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    enum JsItem {
        End,
        Regex,
        StringDouble,
        StringSingle,
        TmplLiteral,
        LineComment,
        MultiLineComment,
        // just here to ignore and to prevent the parser from skipping past multiple of them
        Curly,
    }
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    enum JsItemOrCurly {
        JsItem(JsItem),
        Curly(CurlyKind),
    }

    fn handle_js_item<'s>(
        inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
        tmpl_literal_brace_counts: &mut Vec<usize>,
        item: JsItem,
    ) -> Option<JsState> {
        match item {
            JsItem::End => {
                return Some(JsState {
                    tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
                    sub_state: JsSubState::Regular,
                });
            }
            JsItem::Regex => {
                if let Some(state) = parse_js_regex(inp, tmpl_literal_brace_counts.clone(), false) {
                    return Some(state);
                }
            }
            JsItem::StringDouble => {
                if inp.parse(js_string_parser('"')).expect(INCLUDED_END_FAIL) {
                    return Some(JsState {
                        tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
                        sub_state: JsSubState::StringDouble,
                    });
                }
            }
            JsItem::StringSingle => {
                if inp.parse(js_string_parser('\'')).expect(INCLUDED_END_FAIL) {
                    return Some(JsState {
                        tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
                        sub_state: JsSubState::StringSingle,
                    });
                }
            }
            JsItem::TmplLiteral => match parse_js_tmpl_literal(inp, TmplLitState::Regular) {
                JsTmplLiteralItem::ExpressionStart => {
                    tmpl_literal_brace_counts.push(1);
                }
                JsTmplLiteralItem::LiteralEnd => (),
                JsTmplLiteralItem::End(state) => {
                    return Some(JsState {
                        tmpl_literal_brace_counts: tmpl_literal_brace_counts.clone(),
                        sub_state: JsSubState::TmplLit(state),
                    });
                }
            },
            JsItem::LineComment => {
                if let Some(state) = parse_js_line_comment(inp, tmpl_literal_brace_counts.clone()) {
                    return Some(state);
                }
            }
            JsItem::MultiLineComment => {
                if let Some(state) =
                    parse_js_multi_line_comment(inp, tmpl_literal_brace_counts.clone())
                {
                    return Some(state);
                }
            }
            JsItem::Curly => (),
        }
        None
    }

    let js_item = choice((
        end().to(JsItem::End),
        choice((
            just('+')
                .repeated()
                .at_least(1)
                .count()
                .filter(|count| count % 2 == 1)
                .or(just('-')
                    .repeated()
                    .at_least(1)
                    .count()
                    .filter(|count| count % 2 == 1))
                .ignored(),
            any()
                .filter(|char: &char| !char.is_ascii_digit())
                .then(just('.'))
                .ignored(),
            one_of(",<>=*%&|^?!~([:;{}").ignored(),
            just("break").ignored(),
            just("case").ignored(),
            just("continue").ignored(),
            just("delete").ignored(),
            just("do").ignored(),
            just("else").ignored(),
            just("finally").ignored(),
            just("in").ignored(),
            just("instanceof").ignored(),
            just("return").ignored(),
            just("throw").ignored(),
            just("try").ignored(),
            just("typeof").ignored(),
            just("void").ignored(),
        ))
        .then(js_whitespace())
        .then(just('/'))
        // this is more of a bandaid. If the input ends with a single slash it is completely
        // unclear whether this starts a regex literal or some kind of comment. Also depends on
        // whether the content after is the sole thing between the two slashes and if so whether it
        // is empty. Ugh.
        // todo: Look further into this and if need be introduce a new state to differentiate the
        // different possibilities
        .ignore_then(one_of("*/").ignored().or(end()).not())
        .to(JsItem::Regex),
        just('"').to(JsItem::StringDouble),
        just("'").to(JsItem::StringSingle),
        just("`").to(JsItem::TmplLiteral),
        choice((just("//"), just("<!--"), just("-->"), just("#!"))).to(JsItem::LineComment),
        just("/*").to(JsItem::MultiLineComment),
        one_of("{}").to(JsItem::Curly),
    ));

    let js_item_or_braces = just('{')
        .to(JsItemOrCurly::Curly(CurlyKind::Opening))
        .or(just('}').to(JsItemOrCurly::Curly(CurlyKind::Closing)))
        .or(js_item.map(JsItemOrCurly::JsItem));

    loop {
        let prev = inp.save();
        match inp
            .parse(
                any()
                    .and_is(js_item_or_braces.not())
                    .repeated()
                    .ignore_then(js_item_or_braces),
            )
            .expect(INCLUDED_END_FAIL)
        {
            JsItemOrCurly::JsItem(item) => {
                if let Some(state) = handle_js_item(inp, &mut tmpl_literal_brace_counts, item) {
                    return state;
                }
            }
            JsItemOrCurly::Curly(curly_kind) => {
                // almost always input should be rewound to correctly capture the context involving
                // curly braces
                let should_rewind = match curly_kind {
                    CurlyKind::Opening => {
                        if let Some(last) = tmpl_literal_brace_counts.last_mut() {
                            *last += 1;
                        }

                        true
                    }
                    CurlyKind::Closing => match tmpl_literal_brace_counts.last_mut() {
                        Some(1) => {
                            tmpl_literal_brace_counts.pop();

                            match parse_js_tmpl_literal(inp, TmplLitState::Regular) {
                                JsTmplLiteralItem::End(state) => {
                                    return JsState {
                                        tmpl_literal_brace_counts,
                                        sub_state: JsSubState::TmplLit(state),
                                    }
                                }
                                JsTmplLiteralItem::LiteralEnd => (),
                                JsTmplLiteralItem::ExpressionStart => {
                                    tmpl_literal_brace_counts.push(1);
                                }
                            }

                            // not however if that would rewind input back into a template literal

                            false
                        }
                        Some(last) => {
                            *last -= 1;

                            true
                        }
                        None => true,
                    },
                };

                if should_rewind {
                    // if we rewind we then parse the actual js_item otherwise skip over the
                    // section and start again
                    inp.rewind(prev);

                    let item = inp
                        .parse(any().and_is(js_item.not()).repeated().ignore_then(js_item))
                        .expect(INCLUDED_END_FAIL);

                    if let Some(state) = handle_js_item(inp, &mut tmpl_literal_brace_counts, item) {
                        return state;
                    }
                }
            }
        }
    }
}

/// a parser that parses a js string up to the delim char and reports whether input ends within it
fn js_string_parser<'src>(c: char) -> impl Parser<'src, &'src str, bool, extra::Default> {
    choice((
        just('\\').then(any()).ignored(),
        any().and_is(just(c).not()).ignored(),
    ))
    .repeated()
    .ignore_then(just(c).to(false).or(end().to(true)))
}

fn parse_js_regex<'src>(
    inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
    tmpl_literal_brace_counts: Vec<usize>,
    in_charset: bool,
) -> Option<JsState> {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    enum RegexItem {
        CharsetBegin,
        CharsetEnd,
        End,
        RegexEnd,
    }
    let element = choice((
        just('[').to(RegexItem::CharsetBegin),
        just(']').to(RegexItem::CharsetEnd),
        just('/').to(RegexItem::RegexEnd),
        end().to(RegexItem::End),
    ));
    let mut in_charset = in_charset;
    loop {
        match inp
            .parse(
                just('\\')
                    .then(any())
                    .ignored()
                    .or(any().and_is(element.not()).ignored())
                    .repeated()
                    .ignore_then(element),
            )
            .expect(INCLUDED_END_FAIL)
        {
            RegexItem::CharsetBegin => in_charset = true,
            RegexItem::CharsetEnd => in_charset = false,
            RegexItem::RegexEnd => break None,
            RegexItem::End => {
                return Some(JsState {
                    tmpl_literal_brace_counts,
                    sub_state: JsSubState::Regex { in_charset },
                })
            }
        }
    }
}

fn parse_js_line_comment<'src>(
    inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
    tmpl_literal_brace_counts: Vec<usize>,
) -> Option<JsState> {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    enum CommentItem {
        End,
        Newline,
    }

    let p = end()
        .to(CommentItem::End)
        .or(newline().to(CommentItem::Newline));
    let p = any().and_is(p.not()).repeated().ignore_then(p);

    match inp.parse(p).expect(INCLUDED_END_FAIL) {
        CommentItem::End => Some(JsState {
            tmpl_literal_brace_counts,
            sub_state: JsSubState::LineComment,
        }),
        CommentItem::Newline => None,
    }
}

fn parse_js_multi_line_comment<'src>(
    inp: &mut InputRef<'src, '_, &'src str, extra::Default>,
    tmpl_literal_brace_counts: Vec<usize>,
) -> Option<JsState> {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    enum CommentItem {
        End,
        CommentEnd,
    }

    let p = end()
        .to(CommentItem::End)
        .or(just("*/").to(CommentItem::CommentEnd));
    let p = any().and_is(p.not()).repeated().ignore_then(p);

    match inp.parse(p).expect(INCLUDED_END_FAIL) {
        CommentItem::End => Some(JsState {
            tmpl_literal_brace_counts,
            sub_state: JsSubState::MultiLineComment,
        }),
        CommentItem::CommentEnd => None,
    }
}

// referenced https://github.com/golang/go/blob/master/src/html/template/js.go

fn is_js(mime: &str) -> bool {
    let mime = mime
        .split_once(';')
        .map_or(mime, |(prefix, _suffix)| prefix)
        .trim()
        .to_lowercase();

    matches!(
        mime.as_str(),
        "application/ecmascript"
            | "application/javascript"
            | "application/json"
            | "application/ld+json"
            | "application/x-ecmascript"
            | "application/x-javascript"
            | "module"
            | "text/ecmascript"
            | "text/javascript"
            | "text/javascript1."
            | "text/javascript1.1"
            | "text/javascript1.2"
            | "text/javascript1.3"
            | "text/javascript1.4"
            | "text/javascript1.5"
            | "text/jscript"
            | "text/livescript"
            | "text/x-ecmascript"
            | "text/x-javascript"
    )
}

include!(concat!(env!("OUT_DIR"), "/entities_generated.rs"));

fn unescape_entites(s: &str) -> Result<Cow<'_, str>, ()> {
    let unicode_entity = just::<_, _, extra::Default>("&#")
        .ignore_then(
            one_of("xX")
                .ignore_then(int(16).try_map(|hex, _| {
                    u32::from_str_radix(hex, 16).map_err(|_| EmptyErr::default())
                }))
                .or(int(10)
                    .try_map(|decimal, _| str::parse(decimal).map_err(|_| EmptyErr::default()))),
        )
        .try_map(|code, _| char::from_u32(code).ok_or(EmptyErr::default()))
        .map(|char| Cow::Owned(char.to_string()));
    let named_entity = just('&')
        .then(any().filter(char::is_ascii_alphanumeric).repeated())
        .to_slice()
        .try_map(|entity: &str, _| {
            ENTITY_MAP
                .get(&entity.to_lowercase())
                .ok_or(EmptyErr::default())
                .copied()
        })
        .map(Cow::Borrowed);

    let entity = unicode_entity
        .or(named_entity)
        .then_ignore(just(';').or_not());

    let parser = IterParser::collect::<Vec<Cow<str>>>(
        entity
            .or(any()
                .and_is(entity.not())
                .repeated()
                .at_least(1)
                .to_slice()
                .map(Cow::Borrowed))
            .repeated(),
    );

    let cows: Vec<Cow<str>> = parser.parse(s).into_result().map_err(|_| ())?;

    Ok(match cows.len() {
        0 => Cow::Borrowed(""),
        1 => cows.into_iter().next().expect("vec has length 1"),
        _ => {
            let mut res = String::with_capacity(cows.iter().map(|cow| cow.len()).sum());

            for cow in cows {
                res.push_str(&cow);
            }

            Cow::Owned(res)
        }
    })
}

fn attr_value_single_parser<'src>() -> impl Parser<'src, &'src str, &'src str> + Copy {
    any().and_is(just('\'').not()).repeated().to_slice()
}

fn attr_value_double_parser<'src>() -> impl Parser<'src, &'src str, &'src str> + Copy {
    any().and_is(just('"').not()).repeated().to_slice()
}

fn attr_value_bare_parser<'src>() -> impl Parser<'src, &'src str, &'src str> + Copy {
    any()
        .and_is(html_whitespace().at_least(1).or(just('>').ignored()).not())
        .repeated()
        .to_slice()
}

/// returns the unescaped value and the delimiter kind in case the input ended in the value
fn parse_attr_value<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
) -> Result<(Cow<'s, str>, Option<DelimiterKind>), ContextualizationError> {
    let ((delimiter, value), end) = inp
        .parse(
            choice((
                just('\'').ignore_then(
                    attr_value_single_parser().map(|value| (DelimiterKind::Single, value)),
                ),
                just('"').ignore_then(
                    attr_value_double_parser().map(|value| (DelimiterKind::Double, value)),
                ),
                attr_value_bare_parser().map(|value| (DelimiterKind::Bare, value)),
            ))
            .then(end().or_not().map(|end| end.is_some())),
        )
        .map_err(|_| ContextualizationError::InvalidStatic)?;

    let value = unescape_entites(value).map_err(|()| ContextualizationError::InvalidStatic)?;

    if end {
        return Ok((value, true.then_some(delimiter)));
    }

    let end_char = match delimiter {
        DelimiterKind::Bare => return Ok((value, end.then_some(delimiter))),
        DelimiterKind::Single => '\'',
        DelimiterKind::Double => '"',
    };

    inp.parse(just(end_char))
        .map_err(|_| ContextualizationError::InvalidStatic)?;

    Ok((value, end.then_some(delimiter)))
}

fn css_whitespace<'s>(
) -> Repeated<impl Parser<'s, &'s str, (), extra::Default> + Copy, (), &'s str, extra::Default> {
    choice((
        one_of(" \t\n\u{0C}").ignored(),
        just("\r\n").ignored(),
        just('\r').ignored(),
    ))
    .repeated()
}

fn parse_css<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
) -> Result<CssState, ContextualizationError> {
    #[derive(Clone, Copy)]
    enum CssItem {
        End,
        Url(DelimiterKind),
        LineComment,
        MultiLineComment,
        StringSingle,
        StringDouble,
    }

    let item = choice((
        end().to(CssItem::End),
        empty()
            .or(any().filter(|c: &char| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '\u{80}'..='\u{d7ff}' | '\u{e000}' ..= '\u{fffd}' | '\u{10000}' ..= '\u{10ffff}')).ignored())
            .then(just_case_insensitive("url"))
            .then(css_whitespace())
            .then(just('('))
            .then(css_whitespace())
            .ignore_then(choice((
                just('"').to(DelimiterKind::Double),
                just('\'').to(DelimiterKind::Single),
                empty().to(DelimiterKind::Bare),
            )))
            .map(CssItem::Url),
        just("//").to(CssItem::LineComment),
        just("/*").to(CssItem::MultiLineComment),
        just('"').to(CssItem::StringDouble),
        just('\'').to(CssItem::StringSingle),
    ));

    loop {
        match inp
            .parse(any().and_is(item.not()).repeated().ignore_then(item))
            .map_err(|_| ContextualizationError::InvalidStatic)?
        {
            CssItem::End => return Ok(CssState::Regular),
            CssItem::Url(delim) => {
                if parse_css_string(
                    inp,
                    one_of(match delim {
                        // referenced https://www.w3.org/TR/css-syntax-3/#consume-a-url-token; even
                        // though this appears to conflict with the go implementation
                        DelimiterKind::Bare => ")",
                        DelimiterKind::Double => "\"",
                        DelimiterKind::Single => "'",
                    })
                    .ignored(),
                ) {
                    return Ok(CssState::String(delim));
                }
            }
            CssItem::StringDouble => {
                if parse_css_string(inp, just('"').ignored()) {
                    return Ok(CssState::String(DelimiterKind::Double));
                }
            }
            CssItem::StringSingle => {
                if parse_css_string(inp, just('\'').ignored()) {
                    return Ok(CssState::String(DelimiterKind::Single));
                }
            }
            CssItem::LineComment => {
                if parse_css_comment(inp, one_of("\n\x0C\r").ignored()) {
                    return Ok(CssState::LineComment);
                }
            }
            CssItem::MultiLineComment => {
                if parse_css_comment(inp, just("*/").ignored()) {
                    return Ok(CssState::MultiLineComment);
                }
            }
        }
    }
}

fn parse_css_string<'s>(
    inp: &mut InputRef<'s, '_, &'s str, extra::Default>,
    endp: impl Parser<'s, &'s str, (), extra::Default> + Clone,
) -> bool {
    inp.parse(
        just('\\')
            .ignore_then(any())
            .or(any().and_is(endp.clone().not()))
            .repeated()
            .ignore_then(endp.to(false).or(end().to(true))),
    )
    .expect(INCLUDED_END_FAIL)
}

/// returns whether input ends within css comment
fn parse_css_comment<'s, 'a>(
    inp: &mut InputRef<'s, 'a, &'s str, extra::Default>,
    endp: impl Parser<'s, &'s str, ()> + Copy,
) -> bool {
    inp.parse(
        any()
            .and_is(endp.not())
            .repeated()
            .ignore_then(endp.to(false).or(end().to(true))),
    )
    .expect(INCLUDED_END_FAIL)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DelimiterKind {
    Bare,
    Single,
    Double,
}

impl DelimiterKind {
    fn name_parser<'src>() -> impl Parser<'src, &'src str, DelimiterKind> + Clone {
        choice((
            just('b').to(DelimiterKind::Bare),
            just('s').to(DelimiterKind::Single),
            just('d').to(DelimiterKind::Double),
        ))
    }
}

impl Display for DelimiterKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            DelimiterKind::Bare => "b",
            DelimiterKind::Single => "s",
            DelimiterKind::Double => "d",
        })
    }
}

// referenced https://github.com/golang/go/blob/master/src/html/template/attr.go

#[derive(Debug, Clone, Copy)]
enum AttributeKind {
    Plain,
    Html,
    Css,
    Js,
    Unsafe,
}

impl From<&str> for AttributeKind {
    fn from(value: &str) -> Self {
        let value = if let Some(remainder) = value.strip_prefix("data-") {
            remainder
        } else if let Some((_prefix, remainder)) = value.split_once(':') {
            remainder
        } else {
            value
        };

        match value {
            "accept" | "alt" | "autocomplete" | "autofocus" | "autoplay" | "border" | "checked"
            | "class" | "cols" | "colspan" | "contenteditable" | "contextmenu" | "controls"
            | "coords" | "datetime" | "default" | "dir" | "dirname" | "disabled" | "draggable"
            | "dropzone" | "for" | "formtarget" | "headers" | "height" | "hidden" | "high"
            | "hreflang" | "id" | "ismap" | "kind" | "label" | "lang" | "list" | "loop" | "low"
            | "max" | "maxlength" | "media" | "mediagroup" | "min" | "multiple" | "name"
            | "open" | "optimum" | "placeholder" | "preload" | "pubdate" | "radiogroup"
            | "readonly" | "required" | "reversed" | "rows" | "rowspan" | "spellcheck"
            | "scope" | "scoped" | "seamless" | "selected" | "shape" | "size" | "sizes"
            | "span" | "srclang" | "start" | "step" | "tabindex" | "target" | "title" | "width"
            | "wrap" => AttributeKind::Plain,
            "accept-charset" | "async" | "challenge" | "charset" | "content" | "crossorigin"
            | "defer" | "enctype" | "form" | "formenctype" | "formmethod" | "formnovalidate"
            | "http-equiv" | "keytype" | "language" | "method" | "novalidate" | "pattern"
            | "rel" | "sandbox" | "type" | "value" => AttributeKind::Unsafe,
            "srcdoc" => AttributeKind::Html,
            "style" => AttributeKind::Css,
            _ if value.starts_with("on") => AttributeKind::Js,
            _ => AttributeKind::Plain,
        }
    }
}

type Replacement = (&'static str, &'static str);
type Replacements<'a> = &'a [Replacement];

fn merge_replacements<A: IntoIterator<Item = B>, B: IntoIterator<Item = Replacement>>(
    replacements: A,
) -> AhoCorasickEscaper {
    let (mut total_patterns, mut total_replacements): (Vec<&'static str>, Vec<String>) =
        (Vec::new(), Vec::new());

    for replacements in replacements {
        let (patterns, replacements) = replacements.into_iter().fold(
            (Vec::new(), Vec::new()),
            |(mut patterns, mut replacements), (pattern, replacement)| {
                patterns.push(pattern);
                replacements.push(replacement.to_string());

                (patterns, replacements)
            },
        );

        let current_aho_corasick = AhoCorasickBuilder::new()
            .ascii_case_insensitive(true)
            .build(&patterns)
            .expect("failed to build intermediate aho corasick");

        for replacement in &mut total_replacements {
            *replacement = current_aho_corasick.replace_all(replacement, &replacements);
        }

        total_patterns.extend(patterns);
        total_replacements.extend(replacements);
    }

    let searcher = AhoCorasickBuilder::new()
        .ascii_case_insensitive(true)
        .build(total_patterns)
        .expect("failed to build final aho corasick");

    AhoCorasickEscaper {
        searcher,
        replacements: total_replacements,
    }
}

#[cfg(test)]
mod test_merge_replacements {
    use super::{merge_replacements, Replacements};
    use pretty_assertions::assert_eq;

    fn test_escape(replacements: &[Replacements], inp: &'static str, expected_out: &'static str) {
        assert_eq!(
            merge_replacements(
                replacements
                    .iter()
                    .copied()
                    .map(|slice| slice.iter().copied())
            )
            .escape(inp.to_string()),
            expected_out.to_string()
        );
    }

    #[test]
    fn test_simple() {
        test_escape(
            &[&[("a", "b"), ("e", "i")]],
            "hello alberta!",
            "hillo blbirtb!",
        );
    }

    #[test]
    fn test_unrelated() {
        test_escape(
            &[&[("a", "b"), ("c", "d")], &[("e", "f"), ("h", "i")]],
            "abcdefghij",
            "bbddffgiij",
        );
    }

    #[test]
    fn test_chained() {
        test_escape(
            &[
                &[("hello", "much bye"), ("max", "otto")],
                &[("much", "very"), ("tt", "mm")],
            ],
            "hello max",
            "very bye ommo",
        );
    }
}

// referenced https://github.com/golang/go/blob/master/src/html/template/html.go#L144
// this seems to prevent Windows-1252 confused codepoints to get output as is
// I am however not quite sure how necessary this is for actual rust code considering how Rust is
// generally a lot more careful around text encodings and guarantees valid UTF-8 in the String and
// &str types
const IE_BAD_CHARS_REPLACEMENTS: Replacements = &[
    // range 0xfdd0 - 0xfdef
    ("\u{fdd0}", "&#xfdd0;"),
    ("\u{fdd1}", "&#xfdd1;"),
    ("\u{fdd2}", "&#xfdd2;"),
    ("\u{fdd3}", "&#xfdd3;"),
    ("\u{fdd4}", "&#xfdd4;"),
    ("\u{fdd5}", "&#xfdd5;"),
    ("\u{fdd6}", "&#xfdd6;"),
    ("\u{fdd7}", "&#xfdd7;"),
    ("\u{fdd8}", "&#xfdd8;"),
    ("\u{fdd9}", "&#xfdd9;"),
    ("\u{fdda}", "&#xfdda;"),
    ("\u{fddb}", "&#xfddb;"),
    ("\u{fddc}", "&#xfddc;"),
    ("\u{fddd}", "&#xfddd;"),
    ("\u{fdde}", "&#xfdde;"),
    ("\u{fddf}", "&#xfddf;"),
    ("\u{fde0}", "&#xfde0;"),
    ("\u{fde1}", "&#xfde1;"),
    ("\u{fde2}", "&#xfde2;"),
    ("\u{fde3}", "&#xfde3;"),
    ("\u{fde4}", "&#xfde4;"),
    ("\u{fde5}", "&#xfde5;"),
    ("\u{fde6}", "&#xfde6;"),
    ("\u{fde7}", "&#xfde7;"),
    ("\u{fde8}", "&#xfde8;"),
    ("\u{fde9}", "&#xfde9;"),
    ("\u{fdea}", "&#xfdea;"),
    ("\u{fdeb}", "&#xfdeb;"),
    ("\u{fdec}", "&#xfdec;"),
    ("\u{fded}", "&#xfded;"),
    ("\u{fdee}", "&#xfdee;"),
    ("\u{fdef}", "&#xfdef;"),
    // range 0xfff0 to 0xffff
    ("\u{fff0}", "&#xfff0;"),
    ("\u{fff1}", "&#xfff1;"),
    ("\u{fff2}", "&#xfff2;"),
    ("\u{fff3}", "&#xfff3;"),
    ("\u{fff4}", "&#xfff4;"),
    ("\u{fff5}", "&#xfff5;"),
    ("\u{fff6}", "&#xfff6;"),
    ("\u{fff7}", "&#xfff7;"),
    ("\u{fff8}", "&#xfff8;"),
    ("\u{fff9}", "&#xfff9;"),
    ("\u{fffa}", "&#xfffa;"),
    ("\u{fffb}", "&#xfffb;"),
    ("\u{fffc}", "&#xfffc;"),
    ("\u{fffd}", "&#xfffd;"),
    ("\u{fffe}", "&#xfffe;"),
    ("\u{fffe}", "&#xfffe;"),
];

const HTML_REPLACEMENTS: Replacements = &[
    ("\0", "\u{FFFD}"),
    ("&", "&amp;"),
    ("+", "&#43;"),
    ("<", "&lt;"),
    (">", "&gt;"),
    ("'", "&#39;"),
    ("\"", "&#34;"),
];

const HTML_NO_SPACE_REPLACEMENTS: Replacements = &[
    ("\t", "&#9;"),
    ("\n", "&#10;"),
    ("\u{B}", "&#11;"),
    ("\u{C}", "&#12;"),
    ("\r", "&#13;"),
    (" ", "&#32;"),
    ("=", "&#61;"),
    ("`", "&#96;"),
];

const CSS_STRING_REPLACEMENTS: Replacements = &[
    ("\0", "\\0 "),
    ("\t", "\\9 "),
    ("\n", "\\a "),
    ("\u{C}", "\\c "),
    ("\r", "\\d "),
    ("\"", "\\22 "),
    ("'", "\\27 "),
    ("\\", "\\\\"),
    ("<", "\\3c "),
    (">", "\\3e "),
    ("(", "\\28 "),
    (")", "\\29 "),
    // these should not be necessary in theory, but I am rather safe than sorry
    ("&", "\\26 "),
    ("+", "\\2b "),
    ("/", "\\2f "),
    (":", "\\3a "),
    (";", "\\3b "),
    ("{", "\\7b "),
    ("}", "\\7d "),
];

const HTML_COMMENT_REPLACEMENTS: Replacements = &[("-->", "")];

const HTML_CSS_REPLACEMENTS: Replacements = &[("</style", "\\3c \\2f style")];

const CSS_LINE_COMMENT_REPLACEMENTS: Replacements = &[("\n", ""), ("\x0C", ""), ("\r", "")];

const CSS_MULTI_LINE_COMMENT_REPLACEMENTS: Replacements = &[("*/", "")];

const HTML_JS_REPLACEMENTS: Replacements = &[("</script", "\\3c \\2f script")];

const JS_STRING_NO_BACKSLASH_REPLACEMENTS: Replacements = &[
    ("\t", "\\t"),
    ("\n", "\\n"),
    ("\u{000B}", "\\u000b"),
    ("\u{000C}", "\\f"),
    ("\r", "\\r"),
    ("\"", "\\u0022"),
    ("`", "\\u0060"),
    ("'", "\\u0027"),
    // these should not be necessary in theory, but I am rather safe than sorry
    ("&", "\\u0026"),
    ("+", "\\u002b"),
    ("/", "\\/"),
    ("<", "\\u003c"),
    (">", "\\u003e"),
];

const JS_STRING_BACKSLASH_REPLACEMENTS: Replacements = &[("\\", "\\\\")];

const JS_REGEX_REPLACEMENTS: Replacements = &[
    ("$", "\\$"),
    ("(", "\\("),
    (")", "\\)"),
    ("*", "\\*"),
    ("+", "\\u002b"),
    ("-", "\\-"),
    (".", "\\."),
    ("?", "\\?"),
    ("[", "\\["),
    ("]", "\\]"),
    ("^", "\\^"),
    ("{", "\\{"),
    ("}", "\\}"),
    ("|", "\\|"),
];

const JS_TEMPLATE_LITERAL_REPLACEMENTS: Replacements =
    &[("$", "\\u0024"), ("{", "\\u007b"), ("}", "\\u007d")];

impl HasEmptyStategy for ContextualizerState {
    fn empty_strategy(&self) -> EmptyStrategy {
        match self {
            ContextualizerState::Html(HtmlState::ScriptTag(inner)) => inner.empty_strategy(),
            ContextualizerState::Html(HtmlState::StyleTag(inner)) => inner.empty_strategy(),
            ContextualizerState::Html(
                HtmlState::TextAreaTag(inner) | HtmlState::Tag(inner) | HtmlState::TitleTag(inner),
            ) => inner.empty_strategy(),
            ContextualizerState::Css(inner) => inner.empty_strategy(),
            ContextualizerState::Js(inner) => inner.empty_strategy(),
            _ => EmptyStrategy::None,
        }
    }
}

impl HasMultiLineStrategy for ContextualizerState {
    fn multi_line_strategy(&self) -> MultiLineStrategy {
        match self {
            Self::Html(HtmlState::ScriptTag(inner)) => inner.multi_line_strategy(),
            Self::Html(HtmlState::StyleTag(inner)) => inner.multi_line_strategy(),
            ContextualizerState::Html(
                HtmlState::TextAreaTag(inner) | HtmlState::Tag(inner) | HtmlState::TitleTag(inner),
            ) => inner.multi_line_strategy(),
            ContextualizerState::Css(inner) => inner.multi_line_strategy(),
            ContextualizerState::Js(inner) => inner.multi_line_strategy(),
            _ => MultiLineStrategy::None,
        }
    }
}

impl ContextualizerState {
    #[allow(clippy::too_many_lines)]
    fn escape_stages(
        &self,
        other: &ContextualizerState,
    ) -> Result<Vec<Replacements<'static>>, ContextualizationError> {
        macro_rules! attribute_value {
            ($inner:expr) => {{
                let TagSubState::AttributeValue(ref inner, delim) = $inner.sub_state else {
                    unreachable!("due to match arm guard");
                };

                let mut res = inner.escape_stages(other)?;

                match delim {
                    DelimiterKind::Bare => {
                        res.extend([
                            HTML_REPLACEMENTS,
                            HTML_NO_SPACE_REPLACEMENTS,
                            IE_BAD_CHARS_REPLACEMENTS,
                        ]);
                    }
                    DelimiterKind::Single | DelimiterKind::Double => {
                        res.extend([HTML_REPLACEMENTS]);
                    }
                }

                res
            }};
        }

        if self == other {
            return Ok(vec![]);
        }

        Ok(match (self, other) {
            // html / html.{title_tag / text_area_tag} <- text
            (
                ContextualizerState::Html(
                    HtmlState::Regular
                    | HtmlState::TitleTag(TagState {
                        sub_state: TagSubState::Inner(_),
                        ..
                    })
                    | HtmlState::TextAreaTag(TagState {
                        sub_state: TagSubState::Inner(_),
                        ..
                    }),
                ),
                ContextualizerState::PlainText,
            ) => {
                vec![HTML_REPLACEMENTS]
            }

            // html.{tag / text_area_tag / title_tag}.attr_value <- *
            (
                ContextualizerState::Html(
                    HtmlState::Tag(inner)
                    | HtmlState::TextAreaTag(inner)
                    | HtmlState::TitleTag(inner),
                ),
                _,
            ) if matches!(inner.sub_state, TagSubState::AttributeValue(_, _)) => {
                attribute_value!(inner)
            }

            // html.comment <- text
            (ContextualizerState::Html(HtmlState::Comment), ContextualizerState::PlainText) => {
                vec![HTML_COMMENT_REPLACEMENTS]
            }

            // html.style_tag.attr_value <- *
            (ContextualizerState::Html(HtmlState::StyleTag(inner)), _)
                if matches!(inner.sub_state, TagSubState::AttributeValue(_, _)) =>
            {
                attribute_value!(inner)
            }

            // html.style_tag.* <- css / text
            (
                ContextualizerState::Html(HtmlState::StyleTag(TagState {
                    sub_state: TagSubState::Inner(inner),
                    ..
                })),
                ContextualizerState::PlainText | ContextualizerState::Css(_),
            ) => {
                let mut res = vec![HTML_CSS_REPLACEMENTS];
                let inner_self = ContextualizerState::Css(inner.clone());
                res.extend(inner_self.escape_stages(other)?);

                res
            }

            // css.string.{d / s / b} <- text
            (ContextualizerState::Css(CssState::String(_)), ContextualizerState::PlainText) => {
                vec![CSS_STRING_REPLACEMENTS]
            }

            // css.line_comment <- text
            (ContextualizerState::Css(CssState::LineComment), ContextualizerState::PlainText) => {
                vec![CSS_LINE_COMMENT_REPLACEMENTS]
            }

            // css.multiline_comment <- text
            (
                ContextualizerState::Css(CssState::MultiLineComment),
                ContextualizerState::PlainText,
            ) => {
                vec![CSS_MULTI_LINE_COMMENT_REPLACEMENTS]
            }

            // html.script_tag.attribute_value <- *
            (ContextualizerState::Html(HtmlState::ScriptTag(inner)), _)
                if matches!(inner.sub_state, TagSubState::AttributeValue(_, _)) =>
            {
                attribute_value!(inner)
            }

            // html.js_script_tag.* <- js / text
            (
                ContextualizerState::Html(HtmlState::ScriptTag(TagState {
                    sub_state: TagSubState::Inner(ScriptState::Js(inner)),
                    ..
                })),
                ContextualizerState::PlainText | ContextualizerState::Js(_),
            ) => {
                let mut res = vec![HTML_JS_REPLACEMENTS];
                let inner_self = ContextualizerState::Js(inner.clone());
                res.extend(inner_self.escape_stages(other)?);

                res
            }

            // js.* <- js.n{.n}*.*
            (total_first @ ContextualizerState::Js(first), ContextualizerState::Js(second))
                if first.tmpl_literal_brace_counts.is_empty()
                    && !second.tmpl_literal_brace_counts.is_empty() =>
            {
                total_first.escape_stages(&ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: vec![],
                    sub_state: second.sub_state.clone(),
                }))?
            }

            // js.n{.n}*.* <- js.n{.n}*.*
            (
                ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: first_braces,
                    sub_state: first_sub,
                }),
                ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: second_braces,
                    sub_state: second_sub,
                }),
            ) if !first_braces.is_empty()
                && !second_braces.is_empty()
                && first_braces
                    .iter()
                    .zip(second_braces.iter())
                    .all(|(first, second)| first >= second) =>
            {
                ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: vec![],
                    sub_state: first_sub.clone(),
                })
                .escape_stages(&ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: vec![],
                    sub_state: second_sub.clone(),
                }))?
            }

            // js.string.{d / s} <- text
            (
                ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: _,
                    sub_state: JsSubState::StringSingle | JsSubState::StringDouble,
                }),
                ContextualizerState::PlainText,
            ) => vec![
                JS_STRING_BACKSLASH_REPLACEMENTS,
                JS_STRING_NO_BACKSLASH_REPLACEMENTS,
            ],

            // js.regex <- text
            (
                ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: _,
                    sub_state: JsSubState::Regex { in_charset: false },
                }),
                ContextualizerState::PlainText,
            ) => vec![
                JS_STRING_BACKSLASH_REPLACEMENTS,
                JS_REGEX_REPLACEMENTS,
                JS_STRING_NO_BACKSLASH_REPLACEMENTS,
            ],

            // js.tmpl_lit <- text
            (
                ContextualizerState::Js(JsState {
                    tmpl_literal_brace_counts: _,
                    sub_state: JsSubState::TmplLit(TmplLitState::Regular),
                }),
                ContextualizerState::PlainText,
            ) => vec![
                JS_STRING_BACKSLASH_REPLACEMENTS,
                JS_TEMPLATE_LITERAL_REPLACEMENTS,
                JS_STRING_NO_BACKSLASH_REPLACEMENTS,
            ],

            _ => {
                return Err(ContextualizationError::IncompatibleTextType {
                    target_ty: self.to_string(),
                    source_ty: other.to_string(),
                })
            }
        })
    }

    fn escaper(&self, input_ty: &str) -> Result<StandardEscaper, ContextualizationError> {
        let source = ContextualizerState::try_from(input_ty)
            .map_err(|()| ContextualizationError::UnknownTextType(input_ty.to_string()))?;

        let empty_strategy = self.empty_strategy();
        let multi_line_strategy = self.multi_line_strategy();

        Ok(StandardEscaper {
            escaper: merge_replacements(
                self.escape_stages(&source)?
                    .into_iter()
                    .map(|stage| stage.iter().copied()),
            ),
            empty_strategy,
            multi_line_strategy,
        })
    }
}

#[derive(Debug, Default)]
/// The standard [`crate::Contextualizer`] for cookie cutter. It supports html, css and js.
pub struct StandardContextualizer;

impl Contextualizer<StandardEscaper, StandardContextWorker> for StandardContextualizer {
    fn default_text_type(&self) -> String {
        "text".to_string()
    }

    fn contextualize(
        &self,
        text_ty: &str,
    ) -> Result<StandardContextWorker, ContextualizationError> {
        Ok(StandardContextWorker(text_ty.try_into().map_err(|()| {
            ContextualizationError::UnknownTextType(text_ty.to_string())
        })?))
    }
}

#[derive(Debug, Clone)]
/// The standard [`crate::ContextWorker`] for [`StandardContextualizer`].
pub struct StandardContextWorker(ContextualizerState);

impl ContextWorker<StandardEscaper> for StandardContextWorker {
    fn push_static(&mut self, s: &str) -> Result<(), ContextualizationError> {
        self.0 = self
            .0
            .parser()
            .parse(s)
            .into_result()
            .map_err(|_| ContextualizationError::InvalidStatic)?;
        Ok(())
    }

    fn dynamic(&mut self, input_ty: &str) -> Result<StandardEscaper, ContextualizationError> {
        self.0.escaper(input_ty)
    }
}

#[derive(Debug, Clone)]
struct AhoCorasickEscaper {
    searcher: AhoCorasick,
    replacements: Vec<String>,
}

impl AhoCorasickEscaper {
    fn escape(&self, inp: String) -> String {
        let mut iter = self.searcher.find_iter(&inp).peekable();
        if iter.peek().is_none() {
            return inp;
        }

        let mut res = String::with_capacity(inp.len());

        let mut last_match = 0;
        for m in iter {
            // ignore matches starting or stopping in the middle of a multi byte sequence
            if !(inp.is_char_boundary(m.start()) && inp.is_char_boundary(m.end())) {
                continue;
            }

            res.push_str(&inp[last_match..m.start()]);
            last_match = m.end();

            res.push_str(&self.replacements[m.pattern()]);
        }

        res.push_str(&inp[last_match..]);

        res
    }
}

#[derive(Debug, Clone)]
/// The standard [`crate::Escaper`] for [`StandardContextualizer`].
pub struct StandardEscaper {
    escaper: AhoCorasickEscaper,
    empty_strategy: EmptyStrategy,
    multi_line_strategy: MultiLineStrategy,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum EmptyStrategy {
    None,
    Deny,
    /// replace empty with (?:)
    ReplaceRegex,
}

trait HasEmptyStategy {
    fn empty_strategy(&self) -> EmptyStrategy;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum MultiLineStrategy {
    None,
    Condense,
}

trait HasMultiLineStrategy {
    fn multi_line_strategy(&self) -> MultiLineStrategy;
}

const FAIL_DENY_EMPTY: &str = "escaping result may not be empty";

impl Escaper for StandardEscaper {
    fn escape<'a>(&self, inp: Value) -> Result<Value, String> {
        let out = self.escaper.escape(match inp {
            Value::Text(inp) => match (self.empty_strategy, inp.is_empty()) {
                (_, false) | (EmptyStrategy::None, _) => inp,
                (EmptyStrategy::Deny, true) => return Err(FAIL_DENY_EMPTY.to_string()),
                (EmptyStrategy::ReplaceRegex, true) => "(?:)".to_string(),
            },
            Value::Bool(value) => value.to_string(),
            Value::Float(value) => value.to_string(),
            Value::Int(value) => value.to_string(),
            Value::ArrayOrTuple(ref values) => {
                if self.multi_line_strategy == MultiLineStrategy::Condense {
                    return self.escape(inp.clone().try_into().map(Value::Text).unwrap_or(inp));
                }

                let values: Vec<_> = values
                    .iter()
                    .map(|value| self.escape(value.clone()))
                    .collect::<Result<_, _>>()?;

                let is_empty = values
                    .iter()
                    .all(|value| matches!(value, Value::Text(s) if s.is_empty()));

                match (self.empty_strategy, is_empty) {
                    (_, false) | (EmptyStrategy::None, _) => (),
                    (EmptyStrategy::Deny, true) => return Err(FAIL_DENY_EMPTY.to_string()),
                    (EmptyStrategy::ReplaceRegex, true) => {
                        return Ok(Value::Text("(?:)".to_string()))
                    }
                }

                return Ok(Value::ArrayOrTuple(values));
            }
            inp @ Value::Struct(_) => return Ok(inp),
        });

        Ok(Value::Text(out))
    }
}
