//! Custom LSP definitions and protocol conversions.

use core::fmt;

use hir::Mutability;
use ide::{CompletionItem, CompletionItemImport, CompletionItemRefMode, CompletionRelevance};
use tenthash::TentHash;

pub mod ext;

pub(crate) mod capabilities;
pub(crate) mod from_proto;
pub(crate) mod semantic_tokens;
pub(crate) mod to_proto;
pub(crate) mod utils;

#[derive(Debug)]
pub(crate) struct LspError {
    pub(crate) code: i32,
    pub(crate) message: String,
}

impl LspError {
    pub(crate) fn new(code: i32, message: String) -> LspError {
        LspError { code, message }
    }
}

impl fmt::Display for LspError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Language Server request failed with {}. ({})", self.code, self.message)
    }
}

impl std::error::Error for LspError {}

pub(crate) fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8; 20] {
    fn hash_completion_relevance(hasher: &mut TentHash, relevance: &CompletionRelevance) {
        use ide_completion::{
            CompletionRelevancePostfixMatch, CompletionRelevanceReturnType,
            CompletionRelevanceTypeMatch,
        };

        hasher.update([
            u8::from(relevance.exact_name_match),
            u8::from(relevance.is_local),
            u8::from(relevance.is_name_already_imported),
            u8::from(relevance.requires_import),
            u8::from(relevance.is_private_editable),
        ]);

        match relevance.type_match {
            None => hasher.update([0u8]),
            Some(CompletionRelevanceTypeMatch::CouldUnify) => hasher.update([1u8]),
            Some(CompletionRelevanceTypeMatch::Exact) => hasher.update([2u8]),
        }

        hasher.update([u8::from(relevance.trait_.is_some())]);
        if let Some(trait_) = &relevance.trait_ {
            hasher.update([u8::from(trait_.is_op_method), u8::from(trait_.notable_trait)]);
        }

        match relevance.postfix_match {
            None => hasher.update([0u8]),
            Some(CompletionRelevancePostfixMatch::NonExact) => hasher.update([1u8]),
            Some(CompletionRelevancePostfixMatch::Exact) => hasher.update([2u8]),
        }

        hasher.update([u8::from(relevance.function.is_some())]);
        if let Some(function) = &relevance.function {
            hasher.update([u8::from(function.has_params), u8::from(function.has_self_param)]);
            let discriminant: u8 = match function.return_type {
                CompletionRelevanceReturnType::Other => 0,
                CompletionRelevanceReturnType::DirectConstructor => 1,
                CompletionRelevanceReturnType::Constructor => 2,
                CompletionRelevanceReturnType::Builder => 3,
            };
            hasher.update([discriminant]);
        }
    }

    let mut hasher = TentHash::new();
    hasher.update([
        u8::from(is_ref_completion),
        u8::from(item.is_snippet),
        u8::from(item.deprecated),
        u8::from(item.trigger_call_info),
    ]);

    hasher.update(item.label.primary.len().to_ne_bytes());
    hasher.update(&item.label.primary);

    hasher.update([u8::from(item.label.detail_left.is_some())]);
    if let Some(label_detail) = &item.label.detail_left {
        hasher.update(label_detail.len().to_ne_bytes());
        hasher.update(label_detail);
    }

    hasher.update([u8::from(item.label.detail_right.is_some())]);
    if let Some(label_detail) = &item.label.detail_right {
        hasher.update(label_detail.len().to_ne_bytes());
        hasher.update(label_detail);
    }

    // NB: do not hash edits or source range, as those may change between the time the client sends the resolve request
    // and the time it receives it: some editors do allow changing the buffer between that, leading to ranges being different.
    //
    // Documentation hashing is skipped too, as it's a large blob to process,
    // while not really making completion properties more unique as they are already.

    let kind_tag = item.kind.tag();
    hasher.update(kind_tag.len().to_ne_bytes());
    hasher.update(kind_tag);

    hasher.update(item.lookup.len().to_ne_bytes());
    hasher.update(&item.lookup);

    hasher.update([u8::from(item.detail.is_some())]);
    if let Some(detail) = &item.detail {
        hasher.update(detail.len().to_ne_bytes());
        hasher.update(detail);
    }

    hash_completion_relevance(&mut hasher, &item.relevance);

    hasher.update([u8::from(item.ref_match.is_some())]);
    if let Some((ref_mode, text_size)) = &item.ref_match {
        let discriminant = match ref_mode {
            CompletionItemRefMode::Reference(Mutability::Shared) => 0u8,
            CompletionItemRefMode::Reference(Mutability::Mut) => 1u8,
            CompletionItemRefMode::Dereference => 2u8,
        };
        hasher.update([discriminant]);
        hasher.update(u32::from(*text_size).to_ne_bytes());
    }

    hasher.update(item.import_to_add.len().to_ne_bytes());
    for import_path in &item.import_to_add {
        let CompletionItemImport { path, as_underscore } = import_path;
        hasher.update(path.len().to_ne_bytes());
        hasher.update(path);
        hasher.update([u8::from(*as_underscore)]);
    }

    hasher.finalize()
}
