use memchr::memmem::{Finder, FinderRev};

use oxc_span::SourceType;

use crate::frameworks::FrameworkOptions;

use super::{
    COMMENT_END, COMMENT_START, JavaScriptSource, SCRIPT_END, SCRIPT_START,
    find_script_closing_angle, find_script_start,
};

pub struct VuePartialLoader<'a> {
    source_text: &'a str,
}

impl<'a> VuePartialLoader<'a> {
    pub fn new(source_text: &'a str) -> Self {
        Self { source_text }
    }

    pub fn parse(self) -> Vec<JavaScriptSource<'a>> {
        self.parse_scripts()
    }

    /// Each *.vue file can contain at most
    ///  * one `<script>` block (excluding `<script setup>`).
    ///  * one `<script setup>` block (excluding normal `<script>`).
    ///    <https://vuejs.org/api/sfc-spec.html#script>
    fn parse_scripts(&self) -> Vec<JavaScriptSource<'a>> {
        let mut pointer = 0;
        let Some(result1) = self.parse_script(&mut pointer) else {
            return vec![];
        };
        let Some(result2) = self.parse_script(&mut pointer) else {
            return vec![result1];
        };
        vec![result1, result2]
    }

    fn parse_script(&self, pointer: &mut usize) -> Option<JavaScriptSource<'a>> {
        let script_start_finder = Finder::new(SCRIPT_START);
        let comment_start_finder = FinderRev::new(COMMENT_START);
        let comment_end_finder: Finder<'_> = Finder::new(COMMENT_END);
        // find opening "<script"
        *pointer += find_script_start(
            self.source_text,
            *pointer,
            &script_start_finder,
            &comment_start_finder,
            &comment_end_finder,
        )?;

        // skip `<script-`
        if !self.source_text[*pointer..].starts_with([' ', '>']) {
            return self.parse_script(pointer);
        }

        // find closing ">"
        let offset = find_script_closing_angle(self.source_text, *pointer)?;

        // get ts and jsx attribute
        let content = &self.source_text[*pointer..*pointer + offset];

        // parse `lang`
        let lang = Self::extract_lang_attribute(content);
        let is_setup = content.contains("setup"); // check if "setup" is present, does not check if its inside an attribute

        let Ok(mut source_type) = SourceType::from_extension(lang) else { return None };
        // Vue script blocks are ESM modules - upgrade unambiguous to module
        if source_type.is_unambiguous() {
            source_type = source_type.with_module(true);
        }
        if !lang.contains('x') {
            source_type = source_type.with_standard(true);
        }

        *pointer += offset + 1;
        let js_start = *pointer;

        // find "</script>"
        let script_end_finder: Finder<'_> = Finder::new(SCRIPT_END);
        let offset = script_end_finder.find(&self.source_text.as_bytes()[*pointer..])?;
        let js_end = *pointer + offset;
        *pointer += offset + SCRIPT_END.len();

        let source_text = &self.source_text[js_start..js_end];
        // NOTE: loader checked that source_text.len() is less than u32::MAX
        #[expect(clippy::cast_possible_truncation)]
        Some(JavaScriptSource::partial_with_framework_options(
            source_text,
            source_type,
            if is_setup { FrameworkOptions::VueSetup } else { FrameworkOptions::Default },
            js_start as u32,
        ))
    }

    fn extract_lang_attribute(content: &str) -> &str {
        let content = content.trim();

        let Some(lang_index) = content.find("lang") else { return "mjs" };

        // Move past "lang"
        let mut rest = content[lang_index + 4..].trim_start();

        if !rest.starts_with('=') {
            return "mjs";
        }

        // Move past "="
        rest = rest[1..].trim_start();

        let first_char = rest.chars().next();

        match first_char {
            Some('"' | '\'') => {
                let quote = first_char.unwrap();
                rest = &rest[1..];
                match rest.find(quote) {
                    Some(end) => &rest[..end],
                    None => "mjs", // Unterminated quote
                }
            }
            Some(_) => {
                // Unquoted value: take until first whitespace or attribute separator
                match rest.find(|c: char| c.is_whitespace() || c == '>') {
                    Some(end) => &rest[..end],
                    None => rest, // whole rest is the lang value
                }
            }
            None => "mjs", // nothing after =
        }
    }
}

#[cfg(test)]
mod test {
    use super::{JavaScriptSource, VuePartialLoader};

    fn parse_vue(source_text: &str) -> JavaScriptSource<'_> {
        let sources = VuePartialLoader::new(source_text).parse();
        *sources.first().unwrap()
    }

    #[test]
    fn test_parse_vue_one_line() {
        let source_text = r#"
        <template>
          <h1>hello world</h1>
        </template>
        <script> console.log("hi") </script>
        "#;

        let result = parse_vue(source_text);
        assert_eq!(result.source_text, r#" console.log("hi") "#);
    }

    #[test]
    fn test_build_vue_with_ts_flag_1() {
        let source_text = r#"
        <script lang="ts" setup generic="T extends Record<string, string>">
            1/1
        </script>
        "#;

        let result = parse_vue(source_text);
        assert!(result.source_type.is_typescript());
        assert!(!result.source_type.is_jsx());
        assert_eq!(result.source_text.trim(), "1/1");
    }

    #[test]
    fn test_build_vue_with_ts_flag_3() {
        let source_text = r"
        <script lang='ts' setup>
            1/1
        </script>
        ";

        let result = parse_vue(source_text);
        assert!(result.source_type.is_typescript());
        assert!(!result.source_type.is_jsx());
        assert_eq!(result.source_text.trim(), "1/1");
    }

    #[test]
    fn test_build_vue_with_escape_string() {
        let source_text = r"
        <script setup>
            a.replace(/&#39;/g, '\''))
        </script>
        <template> </template>
        ";

        let result = parse_vue(source_text);
        assert!(!result.source_type.is_typescript());
        assert_eq!(result.source_text.trim(), r"a.replace(/&#39;/g, '\''))");
    }

    #[test]
    fn test_multi_level_template_literal() {
        let source_text = r"
        <script setup>
            `a${b( `c \`${d}\``)}`
        </script>
        ";

        let result = parse_vue(source_text);
        assert_eq!(result.source_text.trim(), r"`a${b( `c \`${d}\``)}`");
    }

    #[test]
    fn test_brace_with_regex_in_template_literal() {
        let source_text = r"
        <script setup>
            `${/{/}`
        </script>
        ";

        let result = parse_vue(source_text);
        assert_eq!(result.source_text.trim(), r"`${/{/}`");
    }

    #[test]
    fn test_no_script() {
        let source_text = r"
            <template></template>
        ";

        let sources = VuePartialLoader::new(source_text).parse();
        assert!(sources.is_empty());
    }

    #[test]
    fn test_syntax_error() {
        let source_text = r"
        <script>
            console.log('error')
        ";
        let sources = VuePartialLoader::new(source_text).parse();
        assert!(sources.is_empty());
    }

    #[test]
    fn test_multiple_scripts() {
        let source_text = r"
        <template></template>
        <script>a</script>
        <script setup>b</script>
        ";
        let sources = VuePartialLoader::new(source_text).parse();
        assert_eq!(sources.len(), 2);
        assert_eq!(sources[0].source_text, "a");
        assert_eq!(sources[1].source_text, "b");
    }

    #[test]
    fn test_unicode() {
        let source_text = r"
        <script setup>
        let 日历 = '2000年';
        const t = useTranslate({
            'zh-CN': {
                calendar: '日历',
                tiledDisplay: '平铺展示',
            },
        });
        </script>
        ";

        let result = parse_vue(source_text);
        assert_eq!(
            result.source_text.trim(),
            "let 日历 = '2000年';
        const t = useTranslate({
            'zh-CN': {
                calendar: '日历',
                tiledDisplay: '平铺展示',
            },
        });"
            .trim()
        );
    }

    #[test]
    fn test_script_in_template() {
        let source_text = r"
        <template><script-view /></template>
        <script>a</script>
        ";
        let sources = VuePartialLoader::new(source_text).parse();
        assert_eq!(sources.len(), 1);
        assert_eq!(sources[0].source_text, "a");
    }

    #[test]
    fn test_closing_character_inside_attribute() {
        let source_text = r"
        <script description='PI > 5'>a</script>
        ";

        let result = parse_vue(source_text);
        assert_eq!(result.source_text, "a");
    }

    #[test]
    fn test_script_inside_code_comment() {
        let source_text = r"
        <!-- <script>a</script> -->
        <!-- <script> -->
        <script>b</script>
        ";

        let result: JavaScriptSource<'_> = parse_vue(source_text);
        assert_eq!(result.source_text, "b");
        assert_eq!(result.start, 79);
    }

    #[test]
    #[expect(clippy::type_complexity)]
    fn lang() {
        // Test cases: (source_text, expected_is_typescript, expected_is_jsx, expected_is_module)
        // None means parsing should fail (invalid extension)
        // Unambiguous source types are upgraded to module for Vue scripts
        let cases: [(&str, Option<(bool, bool, bool)>); 8] = [
            ("<script>debugger</script>", Some((false, false, true))), // mjs -> module
            ("<script lang = 'tsx' >debugger</script>", Some((true, true, true))), // tsx -> unambiguous -> module
            (r#"<script lang = "cjs" >debugger</script>"#, Some((false, false, false))), // cjs -> script (not upgraded)
            ("<script lang=tsx>debugger</script>", Some((true, true, true))), // tsx -> unambiguous -> module
            ("<script lang = 'xxx'>debugger</script>", None),
            (r#"<script lang = "xxx">debugger</script>"#, None),
            ("<script lang='xxx'>debugger</script>", None),
            (r#"<script lang="xxx">debugger</script>"#, None),
        ];

        for (source_text, expected) in cases {
            let sources = VuePartialLoader::new(source_text).parse();
            if let Some((is_ts, is_jsx, is_module)) = expected {
                assert_eq!(sources.len(), 1, "Expected 1 source for: {source_text}");
                let st = sources[0].source_type;
                assert_eq!(st.is_typescript(), is_ts, "is_typescript mismatch for: {source_text}");
                assert_eq!(st.is_jsx(), is_jsx, "is_jsx mismatch for: {source_text}");
                assert_eq!(st.is_module(), is_module, "is_module mismatch for: {source_text}");
            } else {
                assert_eq!(sources.len(), 0, "Expected 0 sources for: {source_text}");
            }
        }
    }
}
