// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2026 Jarkko Sakkinen

use anyhow::{Context, Result};
use serde::{Serialize, Serializer};
use std::path::Path;
use std::sync::OnceLock;
use strum_macros::{Display, EnumString};
use syntect::parsing::SyntaxSet;

#[repr(u16)]
#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub(crate) enum Language {
    Assembly = 0,
    C = 1,
    Bash = 2,
    Cpp = 3,
    CSharp = 4,
    Css = 5,
    Dockerfile = 6,
    Go = 7,
    Gdscript = 8,
    Java = 9,
    JavaScript = 10,
    Jsx = 11,
    Html = 12,
    Json = 13,
    Kconfig = 14,
    Latex = 15,
    Lua = 16,
    Markdown = 17,
    Xml = 18,
    Yaml = 19,
    Just = 20,
    Make = 21,
    Meson = 22,
    Nix = 23,
    Perl = 24,
    Python = 25,
    Php = 26,
    Puppet = 27,
    Ruby = 28,
    Riscv = 29,
    Rust = 30,
    Swift = 31,
    Sql = 32,
    TypeScript = 33,
    Typst = 34,
    Toml = 35,
    Tsx = 36,
    Vimscript = 37,
    Zig = 38,
    Unknown = 39,
}

const LANGUAGE_VARIANTS: [Language; 40] = [
    Language::Assembly,
    Language::C,
    Language::Bash,
    Language::Cpp,
    Language::CSharp,
    Language::Css,
    Language::Dockerfile,
    Language::Go,
    Language::Gdscript,
    Language::Java,
    Language::JavaScript,
    Language::Jsx,
    Language::Html,
    Language::Json,
    Language::Kconfig,
    Language::Latex,
    Language::Lua,
    Language::Markdown,
    Language::Xml,
    Language::Yaml,
    Language::Just,
    Language::Make,
    Language::Meson,
    Language::Nix,
    Language::Perl,
    Language::Python,
    Language::Php,
    Language::Puppet,
    Language::Ruby,
    Language::Riscv,
    Language::Rust,
    Language::Swift,
    Language::Sql,
    Language::TypeScript,
    Language::Typst,
    Language::Toml,
    Language::Tsx,
    Language::Vimscript,
    Language::Zig,
    Language::Unknown,
];

impl From<Language> for u16 {
    fn from(language: Language) -> Self {
        language as u16
    }
}

impl TryFrom<u16> for Language {
    type Error = anyhow::Error;

    fn try_from(value: u16) -> Result<Self, Self::Error> {
        LANGUAGE_VARIANTS
            .get(value as usize)
            .copied()
            .with_context(|| format!("unknown Language discriminant: {value}"))
    }
}

impl Language {
    pub(crate) fn id(self) -> &'static str {
        language_spec(self).map_or("unknown", |spec| spec.id)
    }
}

#[derive(Clone, Copy)]
pub(crate) struct LanguageSpec {
    pub(crate) language: Language,
    pub(crate) id: &'static str,
    pub(crate) engine: Option<AnalysisEngine>,
    pub(crate) aliases: &'static [&'static str],
    pub(crate) extensions: &'static [&'static str],
    pub(crate) file_names: &'static [&'static str],
    pub(crate) syntax_names: &'static [&'static str],
    pub(crate) has_symbols: bool,
}

#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub(crate) enum AnalysisEngine {
    TreeSitter = 0,
    Llvm = 1,
}

impl From<AnalysisEngine> for u8 {
    fn from(engine: AnalysisEngine) -> Self {
        engine as u8
    }
}

const ANALYSIS_ENGINE_VARIANTS: [AnalysisEngine; 2] =
    [AnalysisEngine::TreeSitter, AnalysisEngine::Llvm];

impl TryFrom<u8> for AnalysisEngine {
    type Error = anyhow::Error;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        ANALYSIS_ENGINE_VARIANTS
            .get(value as usize)
            .copied()
            .with_context(|| format!("unknown AnalysisEngine tag: {value}"))
    }
}

#[derive(Clone, Copy, Debug)]
pub(crate) struct EngineField(pub(crate) Option<AnalysisEngine>);

impl Serialize for EngineField {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self.0 {
            Some(e) => e.serialize(serializer),
            None => serializer.serialize_str("none"),
        }
    }
}

pub(crate) const LANGUAGE_SPECS: &[LanguageSpec] = &[
    LanguageSpec {
        language: Language::Assembly,
        id: "assembly",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["assembly", "asm", "x86", "arm"],
        extensions: &["asm", "s", "S"],
        file_names: &[],
        syntax_names: &["Assembly", "ARM Assembly", "x86 Assembly"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::C,
        id: "c",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["c"],
        extensions: &["c", "h"],
        file_names: &[],
        syntax_names: &["C"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Bash,
        id: "bash",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["bash", "sh", "shell"],
        extensions: &["bash", "sh"],
        file_names: &[".bashrc", ".bash_profile", ".profile"],
        syntax_names: &[
            "Bourne Again Shell (bash)",
            "Shell-Unix-Generic",
            "ShellScript",
        ],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Cpp,
        id: "cpp",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["cpp", "cxx", "cplusplus"],
        extensions: &["cc", "cpp", "cxx", "hh", "hpp", "hxx"],
        file_names: &[],
        syntax_names: &["C++"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::CSharp,
        id: "csharp",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["csharp", "cs", "c#"],
        extensions: &["cs"],
        file_names: &[],
        syntax_names: &["C#"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Css,
        id: "css",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["css"],
        extensions: &["css"],
        file_names: &[],
        syntax_names: &["CSS"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Dockerfile,
        id: "dockerfile",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["dockerfile", "containerfile"],
        extensions: &[],
        file_names: &["Dockerfile", "Containerfile"],
        syntax_names: &["Dockerfile"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Go,
        id: "go",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["go", "golang"],
        extensions: &["go"],
        file_names: &["go.mod"],
        syntax_names: &["Go"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Gdscript,
        id: "gdscript",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["gdscript", "gd"],
        extensions: &["gd"],
        file_names: &[],
        syntax_names: &["GDScript"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Java,
        id: "java",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["java"],
        extensions: &["java"],
        file_names: &[],
        syntax_names: &["Java"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::JavaScript,
        id: "javascript",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["javascript", "js"],
        extensions: &["js", "mjs", "cjs"],
        file_names: &[],
        syntax_names: &["JavaScript"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Jsx,
        id: "jsx",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["jsx"],
        extensions: &["jsx"],
        file_names: &[],
        syntax_names: &[],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Html,
        id: "html",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["html", "htm"],
        extensions: &["html", "htm"],
        file_names: &[],
        syntax_names: &["HTML"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Json,
        id: "json",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["json"],
        extensions: &["json"],
        file_names: &["package-lock.json", "composer.lock"],
        syntax_names: &["JSON"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Xml,
        id: "xml",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["xml"],
        extensions: &["xml", "xsd", "xsl", "xslt"],
        file_names: &[],
        syntax_names: &["XML"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Yaml,
        id: "yaml",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["yaml", "yml"],
        extensions: &["yaml", "yml"],
        file_names: &[],
        syntax_names: &["YAML"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Just,
        id: "just",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["just", "justfile"],
        extensions: &["just"],
        file_names: &["justfile", "Justfile", ".justfile"],
        syntax_names: &["Just"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Kconfig,
        id: "kconfig",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["kconfig"],
        extensions: &[],
        file_names: &["Kconfig"],
        syntax_names: &["Kconfig"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Make,
        id: "make",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["make", "makefile"],
        extensions: &["mk", "mak", "make"],
        file_names: &["Makefile", "makefile", "GNUmakefile"],
        syntax_names: &["Makefile"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Latex,
        id: "latex",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["latex", "tex"],
        extensions: &["tex", "ltx", "latex"],
        file_names: &[],
        syntax_names: &["LaTeX", "TeX"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Lua,
        id: "lua",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["lua"],
        extensions: &["lua"],
        file_names: &[],
        syntax_names: &["Lua"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Markdown,
        id: "markdown",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["markdown", "md"],
        extensions: &["md", "markdown", "mdown", "mkd"],
        file_names: &[],
        syntax_names: &["Markdown"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Meson,
        id: "meson",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["meson"],
        extensions: &[],
        file_names: &["meson.build", "meson_options.txt"],
        syntax_names: &["Meson"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Nix,
        id: "nix",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["nix"],
        extensions: &["nix"],
        file_names: &["flake.lock"],
        syntax_names: &["Nix"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Perl,
        id: "perl",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["perl", "pl", "pm"],
        extensions: &["pl", "pm", "t"],
        file_names: &[],
        syntax_names: &["Perl"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Python,
        id: "python",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["python", "py"],
        extensions: &["py", "pyw"],
        file_names: &[],
        syntax_names: &["Python"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Php,
        id: "php",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["php"],
        extensions: &["php", "php3", "php4", "php5", "phtml"],
        file_names: &[],
        syntax_names: &["PHP"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Puppet,
        id: "puppet",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["puppet", "pp"],
        extensions: &["pp"],
        file_names: &["Puppetfile"],
        syntax_names: &["Puppet"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Ruby,
        id: "ruby",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["ruby", "rb"],
        extensions: &["rb", "rake", "gemspec"],
        file_names: &["Gemfile", "Rakefile"],
        syntax_names: &["Ruby"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Riscv,
        id: "riscv",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["riscv", "risc-v", "riscv64"],
        extensions: &["riscv"],
        file_names: &[],
        syntax_names: &["RISC-V"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Rust,
        id: "rust",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["rust", "rs"],
        extensions: &["rs"],
        file_names: &[],
        syntax_names: &["Rust"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Swift,
        id: "swift",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["swift"],
        extensions: &["swift"],
        file_names: &[],
        syntax_names: &["Swift"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Sql,
        id: "sql",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["sql"],
        extensions: &["sql"],
        file_names: &[],
        syntax_names: &["SQL"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::TypeScript,
        id: "typescript",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["typescript", "ts"],
        extensions: &["ts", "mts", "cts"],
        file_names: &[],
        syntax_names: &["TypeScript"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Tsx,
        id: "tsx",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["tsx"],
        extensions: &["tsx"],
        file_names: &[],
        syntax_names: &[],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Toml,
        id: "toml",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["toml"],
        extensions: &["toml"],
        file_names: &["Cargo.lock"],
        syntax_names: &["TOML"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Typst,
        id: "typst",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["typst", "typ"],
        extensions: &["typ"],
        file_names: &[],
        syntax_names: &["Typst"],
        has_symbols: false,
    },
    LanguageSpec {
        language: Language::Vimscript,
        id: "vimscript",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["vimscript", "vim", "viml"],
        extensions: &["vim", "vimrc", "gvimrc"],
        file_names: &[".vimrc", ".gvimrc"],
        syntax_names: &["VimL", "Vimscript"],
        has_symbols: true,
    },
    LanguageSpec {
        language: Language::Zig,
        id: "zig",
        engine: Some(AnalysisEngine::TreeSitter),
        aliases: &["zig"],
        extensions: &["zig", "zon"],
        file_names: &["build.zig", "build.zig.zon"],
        syntax_names: &["Zig"],
        has_symbols: false,
    },
];

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum BinaryMode {
    Reject,
    Lossy,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum DocumentKind {
    Source,
    Text,
}

impl Serialize for Language {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.id())
    }
}

pub(crate) fn detect_language(path: &Path, text: &str) -> Result<(Language, Option<String>)> {
    if let Some(language) = detect_by_path(path) {
        return Ok((language, None));
    }

    // Shebang detection
    if let Some(line) = text.lines().next() {
        if line.starts_with("#!") {
            if line.contains("python") {
                return Ok((Language::Python, None));
            }
            if line.contains("node") {
                return Ok((Language::JavaScript, None));
            }
        }
    }

    let syntax_set = {
        static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
        SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines)
    };
    let syntax = syntax_set
        .find_syntax_for_file(path)
        .with_context(|| format!("detect syntax for {}", path.display()))?;

    let Some(syntax) = syntax else {
        return Ok((Language::Unknown, None));
    };

    Ok((
        LANGUAGE_SPECS
            .iter()
            .find_map(|spec| {
                spec.syntax_names
                    .contains(&syntax.name.as_str())
                    .then_some(spec.language)
            })
            .unwrap_or(Language::Unknown),
        Some(syntax.name.clone()),
    ))
}

pub(crate) fn detect_by_path(path: &Path) -> Option<Language> {
    let file_name = path.file_name()?.to_str()?;
    if let Some(language) = LANGUAGE_SPECS.iter().find_map(|spec| {
        spec.file_names
            .contains(&file_name)
            .then_some(spec.language)
    }) {
        return Some(language);
    }

    let extension = path.extension()?.to_str()?.to_ascii_lowercase();
    LANGUAGE_SPECS.iter().find_map(|spec| {
        spec.extensions
            .contains(&extension.as_str())
            .then_some(spec.language)
    })
}

pub(crate) fn language_spec(language: Language) -> Option<&'static LanguageSpec> {
    LANGUAGE_SPECS.iter().find(|spec| spec.language == language)
}

pub(crate) fn extract_plain_text(
    path: &Path,
    bytes: &[u8],
    binary_mode: BinaryMode,
) -> Result<String> {
    let text = if binary_mode == BinaryMode::Lossy {
        String::from_utf8_lossy(bytes).into_owned()
    } else {
        String::from_utf8(bytes.to_vec())
            .with_context(|| format!("{} is not UTF-8 text", path.display()))?
    };

    Ok(text)
}

pub(crate) fn normalize_source_text(text: &str) -> String {
    let text = text.strip_prefix('\u{feff}').unwrap_or(text);
    if !text.contains('\r') {
        return text.to_owned();
    }
    text.replace("\r\n", "\n").replace('\r', "\n")
}
