use bstr::ByteSlice;
use std::{borrow::Cow, collections::HashMap, io::Write, sync::LazyLock, sync::Mutex};

/// Recognized file types. File types without a custom parser are parsed the same way
/// as `Generic`.
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, serde::Serialize)]
pub enum FileType {
    Generic,
    C,
    Cpp,
    Python,
    Rust,
    Go,
    Javascript,
    Ruby,
    Html,
    Css,
    Php,
    Shell,
    Java,
    Kotlin,
    Yaml,
    Json,
    Toml,
}

#[derive(Clone, Copy, Debug)]
pub struct Tag(u32);

/// Annotates the `data`. Each item in the returned vector corresponds to a line in
/// `data`. `line_count` must be equal to the number of lines in `data`. The parser
/// to use is specified by `file_type`.
pub(crate) fn annotate(data: &[u8], line_count: usize, file_type: FileType) -> Vec<Tag> {
    let mut annotator = ANNOTATOR.lock().unwrap();
    annotator.annotate(data, line_count, file_type)
}

/// Pushes deduplicated tags from `data` into `dest`. The deduplication takes into account
/// the importance of tags.
pub(crate) fn extend_dedup(dest: &mut Vec<Tag>, data: impl IntoIterator<Item = Tag>) {
    for tag in data {
        if let Some(&last) = dest.last() {
            if last == tag {
                if !last.is_important() && tag.is_important() {
                    let index = dest.len() - 1;
                    dest[index] = tag;
                }
                continue;
            }
        }
        dest.push(tag);
    }
}

/// Computes the distance (similarity) between two slices of tags.
pub(crate) fn distance(hunk_tags: &[Tag], line_tags: &[Tag]) -> usize {
    let mut converted: Vec<Tag> = Vec::with_capacity(line_tags.len());
    extend_dedup(&mut converted, line_tags.iter().copied());
    weighted_levenshtein::distance(hunk_tags, &converted)
}

/// Resolve the given tag to its symbol.
pub(crate) fn tag_symbol(tag: Tag) -> Vec<u8> {
    let annotator = ANNOTATOR.lock().unwrap();
    annotator.get_symbol(tag).to_vec()
}

struct Annotator {
    /// Map of allocated tags. Used for symbol -> tag translation.
    tags: HashMap<Vec<u8>, Tag>,
    /// List of allocated tags. Used for tag -> symbol translation.
    rev_tags: Vec<Vec<u8>>,
}

trait AnnotationParser {
    /// Returns annotation tags for `data`. Each item in the returned vector corresponds to
    /// a line in `data`. The returned vector may be smaller than the number of lines in
    /// `data`. In such case, the last tag will be repeated for the remaining lines.
    fn annotate(&self, data: &[u8], annotator: &mut Annotator) -> Vec<Tag>;
}

struct GenericAnnotationParser {
}

struct CAnnotationParser {
    clang: clang::Clang,
    cpp: bool,
}

static ANNOTATOR: LazyLock<Mutex<Annotator>> = LazyLock::new(|| Mutex::new(Annotator::new()));

impl FileType {
    pub(crate) fn from_path(path: &[u8]) -> Self {
        if path.ends_with(b".c") || path.ends_with(b".h") {
            Self::C
        } else if path.ends_with(b".cpp") || path.ends_with(b".cc") || path.ends_with(b".cxx")
            || path.ends_with(b".hpp")
        {
            Self::Cpp
        } else if path.ends_with(b".py") || path.ends_with(b".pyw") {
            Self::Python
        } else if path.ends_with(b".rs") {
            Self::Rust
        } else if path.ends_with(b".go") {
            Self::Go
        } else if path.ends_with(b".js") || path.ends_with(b".jsx") || path.ends_with(b".ts")
            || path.ends_with(b".tsx") || path.ends_with(b".mjs") || path.ends_with(b".cjs")
        {
            Self::Javascript
        } else if path.ends_with(b".rb") || path.ends_with(b".rbw") {
            Self::Ruby
        } else if path.ends_with(b".html") || path.ends_with(b".htm")
            || path.ends_with(b".xml") || path.ends_with(b".xhtml")
        {
            Self::Html
        } else if path.ends_with(b".css") || path.ends_with(b".scss")
            || path.ends_with(b".sass") || path.ends_with(b".less")
        {
            Self::Css
        } else if path.ends_with(b".php") || path.ends_with(b".phtml")
            || path.ends_with(b".php3") || path.ends_with(b".php4") || path.ends_with(b".php5")
        {
            Self::Php
        } else if path.ends_with(b".sh") || path.ends_with(b".bash") || path.ends_with(b".zsh")
            || path.ends_with(b".fish") || path.ends_with(b".csh")
        {
            Self::Shell
        } else if path.ends_with(b".java") {
            Self::Java
        } else if path.ends_with(b".kt") || path.ends_with(b".kts") {
            Self::Kotlin
        } else if path.ends_with(b".yml") || path.ends_with(b".yaml") {
            Self::Yaml
        } else if path.ends_with(b".json") || path.ends_with(b".jsonc") {
            Self::Json
        } else if path.ends_with(b".toml") {
            Self::Toml
        } else {
            Self::Generic
        }
    }
}

impl Tag {
    /// Create the tag representing the portions of the file where no annotation is available.
    pub(crate) fn unknown() -> Self {
        Self(0)
    }

    /// Create tag with the given value.
    fn from(val: u32) -> Self {
        assert!(val > 0);
        Self(val)
    }

    /// Checks if the tag is marked as important.
    pub(crate) fn is_important(&self) -> bool {
        self.0 & 0x8000_0000 != 0
    }

    /// Mark the tag as important or as not important.
    pub(crate) fn set_important(self, important: bool) -> Self {
        let mut val = self.get_value();
        if important {
            val |= 0x8000_0000;
        }
        Self(val)
    }

    fn get_value(&self) -> u32 {
        self.0 & 0x7fff_ffff
    }

    #[cfg(feature = "debug_annotation")]
    pub(crate) fn get_debug_value(&self) -> u32 {
        self.get_value()
    }
}

impl PartialEq for Tag {
    fn eq(&self, other: &Self) -> bool {
        self.get_value() == other.get_value()
    }
}

impl weighted_levenshtein::EditWeight for Tag {
    fn add_cost(&self) -> usize {
        if self.is_important() {
            2
        } else {
            1
        }
    }

    fn rm_cost(&self) -> usize {
        if self.is_important() {
            2
        } else {
            1
        }
    }

    fn sub_cost(&self, other: &Self) -> usize {
        if self == other {
            0
        } else if self.is_important() || other.is_important() {
            2
        } else {
            1
        }
    }
}

impl Annotator {
    fn new() -> Self {
        // The "unknown" Tag indicates there's no symbol associated. It resolves to an empty
        // string.
        let first_tag = Tag::unknown();
        let mut tags = HashMap::new();
        tags.insert(b"".to_vec(), first_tag);
        Self {
            tags,
            rev_tags: vec![b"".to_vec()],
        }
    }

    /// Resolves the given `symbol` into the annotation tag. If the symbol has been previously
    /// added, reuses the tag. Otherwise, allocates a new tag.
    fn get_tag<'a>(&mut self, symbol: impl Into<Cow<'a, [u8]>>) -> Tag {
        let symbol = symbol.into();
        if let Some(&tag) = self.tags.get(symbol.as_ref()) {
            return tag;
        }
        let next_tag = Tag::from(self.rev_tags.len() as u32);
        self.tags.insert(symbol.to_vec(), next_tag);
        self.rev_tags.push(symbol.into_owned());
        next_tag
    }

    /// See the module function `annotate` for details.
    fn annotate(&mut self, data: &[u8], line_count: usize, file_type: FileType) -> Vec<Tag> {
        match file_type {
            FileType::C => self.parse(data, line_count, &CAnnotationParser::new(false)),
            FileType::Cpp => self.parse(data, line_count, &CAnnotationParser::new(true)),
            _ => self.parse(data, line_count, &GenericAnnotationParser::new()),
        }
    }

    /// Annotates the `data` using the given `parser`. See `annotate` for details.
    fn parse(&mut self, data: &[u8], line_count: usize, parser: &impl AnnotationParser)
        -> Vec<Tag>
    {
        let mut result = parser.annotate(data, self);
        self.build_finalize(&mut result, line_count);
        result
    }

    /// A single step in building `dest`. The parameters indicate that `symbol` is at the line
    /// number `line_index` (counted from 0). If there are already resolved tags at and/or
    /// below `line_index`, they are overwritten.
    fn build_push_symbol<'a>(
        &mut self,
        dest: &mut Vec<Tag>,
        line_index: usize,
        symbol: impl Into<Cow<'a, [u8]>>,
    ) {
        let last = dest.last().copied().unwrap_or(Tag::unknown());
        dest.resize(line_index, last);
        dest.push(self.get_tag(symbol));
    }

    /// The last step in building `dest`. Extends it to cover the total number of lines
    /// specified in `line_count`.
    fn build_finalize(&self, dest: &mut Vec<Tag>, line_count: usize) {
        let last = dest.last().copied().unwrap_or(Tag::unknown());
        dest.resize(line_count, last);
    }

    fn get_symbol(&self, tag: Tag) -> &[u8] {
        &self.rev_tags[tag.get_value() as usize]
    }
}

impl GenericAnnotationParser {
    fn new() -> Self {
        Self {}
    }

    fn first_char(bytes: &[u8]) -> Option<char> {
        let part = &bytes[0 .. bytes.len().min(4)];
        let len = match str::from_utf8(part) {
            Ok(s) => return s.chars().next(),
            Err(e) => e.valid_up_to()
        };
        let part = &bytes[0 .. len];
        str::from_utf8(part).ok().and_then(|s| s.chars().next())
    }
}

impl AnnotationParser for GenericAnnotationParser {
    fn annotate(&self, data: &[u8], annotator: &mut Annotator) -> Vec<Tag> {
        // Commit acb7257 in the git project introduces the function names in hunk headers.
        // It states: "it just tries to find a line in the `old' file, from before the
        // start of the hunk, whose first character looks plausible". The definition of
        // "plausible" changed over times to the current isalpha() || '_' || '$' but the
        // mechanism is still used in git as the fallback.
        // Do the same thing with one improvement: take only the first line in each
        // consecutive sequence of matching lines.
        let mut result = Vec::new();
        let mut accept_next = true;
        for (i, line) in data.split(|&c| c == b'\n').enumerate() {
            if matches!(
                Self::first_char(line),
                Some(ch) if ch.is_alphabetic() || ch == '_' || ch == '$'
            ) {
                if accept_next {
                    annotator.build_push_symbol(&mut result, i, line);
                }
                accept_next = false;
            } else {
                accept_next = true;
            }
        }
        result
    }
}

impl CAnnotationParser {
    fn new(cpp: bool) -> Self {
        Self {
            clang: clang::Clang::new().unwrap(),
            cpp,
        }
    }
}

impl AnnotationParser for CAnnotationParser {
    fn annotate(&self, data: &[u8], annotator: &mut Annotator) -> Vec<Tag> {
        let mut result = Vec::new();
        let mut f = tempfile::Builder::new()
            .suffix(if self.cpp { ".cpp" } else { ".c" })
            .tempfile()
            .unwrap();
        f.write_all(data).unwrap();
        f.flush().unwrap();
        let index = clang::Index::new(&self.clang, false, false);
        let mut parser = index.parser(f.path());
        let tu = parser
            .single_file_parse(true)
            .keep_going(true)
            .ignore_non_errors_from_included_files(true)
            .parse()
            .unwrap();
        for child in tu.get_entity().get_children().iter() {
            let mut name = match child.get_name() {
                Some(name) => name.into_bytes(),
                None => continue
            };
            if let Some(index) = name.find(b" (unnamed at") {
                // generalize cases like "enum (unnamed at /tmp/.tmpDIKHpu.c:77:1)" and
                // "struct (unnamed at /tmp/.tmpDIKHpu.c:82:1)"
                name.truncate(index + 9);
                name.push(b')');
            }
            let mut start = match child.get_range() {
                // Line numbers start at 1.
                Some(range) => range.get_start().get_spelling_location().line as usize - 1,
                None => continue
            };
            if let Some(range) = child.get_comment_range() {
                let cmt_start = range.get_start().get_spelling_location().line as usize - 1;
                let cmt_end = range.get_end().get_spelling_location().line as usize;
                // Only consider comments that are right before the symbol. This excludes
                // situations such as:
                //   #ifdef COND
                //   /* function comment */
                //   void function(void) {}
                //   #else
                //   void function(void) {}
                //   #endif
                // where the same "function comment" gets attached to both "function"
                // entities.
                if cmt_end == start {
                    start = cmt_start;
                }
            };
            // Let newer symbols overwrite previous ones at the same line. This cover cases
            // such as:
            //   100  static struct pci_driver my_driver = {
            //   101      ...
            //   102  }
            // where we get:
            //   StructDecl "pci_driver" at 100
            //   VarDecl "my_driver" at 100
            // and want to record "my_driver".
            // Or:
            //   100  typedef int (*my_type)(struct my_struct1 *s1,
            //   101                         struct my_struct2 *s2);
            // where we get:
            //   StructDecl "my_struct1" at 100
            //   StructDecl "my_struct2" at 101
            //   TypeDecl "my_type" at 100
            annotator.build_push_symbol(&mut result, start, name);
        }
        result
    }
}
