use lsp_types::{
    CompletionParams, DocumentHighlightParams, GotoDefinitionParams, HoverParams, Position,
    ReferenceParams, RenameParams, TextDocumentPositionParams,
};
use rowan::{ast::AstNode, TextRange, TextSize};

use crate::{
    syntax::{bibtex, latex},
    DocumentData, LineIndexExt,
};

use super::FeatureRequest;

#[derive(Debug)]
pub enum Cursor {
    Latex(latex::SyntaxToken),
    Bibtex(bibtex::SyntaxToken),
    Nothing,
}

impl Cursor {
    pub fn new_latex(
        left: Option<latex::SyntaxToken>,
        right: Option<latex::SyntaxToken>,
    ) -> Option<Self> {
        let left = left?;
        let right = right?;

        if left.kind().is_command_name() {
            return Some(Self::Latex(left));
        }

        if right.kind() == latex::WORD {
            return Some(Self::Latex(right));
        }

        if left.kind() == latex::WORD {
            return Some(Self::Latex(left));
        }

        if right.kind().is_command_name() {
            return Some(Self::Latex(right));
        }

        if left.kind() == latex::WHITESPACE && left.parent()?.kind() == latex::KEY {
            return Some(Self::Latex(left));
        }

        if matches!(right.kind(), latex::WHITESPACE | latex::LINE_BREAK)
            && right.parent()?.kind() == latex::KEY
        {
            return Some(Self::Latex(right));
        }

        Some(Self::Latex(right))
    }

    pub fn new_bibtex(
        left: Option<bibtex::SyntaxToken>,
        right: Option<bibtex::SyntaxToken>,
    ) -> Option<Self> {
        let left = left?;
        let right = right?;

        if right.kind() == bibtex::TYPE {
            return Some(Self::Bibtex(right));
        }

        if left.kind() == bibtex::TYPE {
            return Some(Self::Bibtex(left));
        }

        if matches!(left.kind(), bibtex::COMMAND_NAME | bibtex::ACCENT_NAME) {
            return Some(Self::Bibtex(left));
        }

        if matches!(right.kind(), bibtex::WORD | bibtex::NAME) {
            return Some(Self::Bibtex(right));
        }

        if matches!(left.kind(), bibtex::WORD | bibtex::NAME) {
            return Some(Self::Bibtex(left));
        }

        if matches!(right.kind(), bibtex::COMMAND_NAME | bibtex::ACCENT_NAME) {
            return Some(Self::Bibtex(right));
        }

        Some(Self::Bibtex(right))
    }

    pub fn as_latex(&self) -> Option<&latex::SyntaxToken> {
        if let Self::Latex(v) = self {
            Some(v)
        } else {
            None
        }
    }

    pub fn as_bibtex(&self) -> Option<&bibtex::SyntaxToken> {
        if let Self::Bibtex(v) = self {
            Some(v)
        } else {
            None
        }
    }

    pub fn command_range(&self, offset: TextSize) -> Option<TextRange> {
        self.as_latex()
            .filter(|token| token.kind().is_command_name())
            .filter(|token| token.text_range().start() != offset)
            .map(|token| token.text_range())
            .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))
            .or_else(|| {
                self.as_bibtex()
                    .filter(|token| {
                        matches!(token.kind(), bibtex::COMMAND_NAME | bibtex::ACCENT_NAME)
                    })
                    .filter(|token| token.text_range().start() != offset)
                    .map(|token| token.text_range())
                    .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))
            })
    }
}

pub struct CursorContext<P> {
    pub request: FeatureRequest<P>,
    pub cursor: Cursor,
    pub offset: TextSize,
}

impl<P: HasPosition> CursorContext<P> {
    pub fn new(request: FeatureRequest<P>) -> Self {
        let main_document = request.main_document();
        let offset = main_document
            .line_index
            .offset_lsp(request.params.position());

        let cursor = match &main_document.data {
            DocumentData::Latex(data) => {
                let root = latex::SyntaxNode::new_root(data.green.clone());
                let left = root.token_at_offset(offset).left_biased();
                let right = root.token_at_offset(offset).right_biased();
                Cursor::new_latex(left, right)
            }
            DocumentData::Bibtex(data) => {
                let root = bibtex::SyntaxNode::new_root(data.green.clone());
                let left = root.token_at_offset(offset).left_biased();
                let right = root.token_at_offset(offset).right_biased();
                Cursor::new_bibtex(left, right)
            }
            DocumentData::BuildLog(_) => None,
        };

        Self {
            request,
            cursor: cursor.unwrap_or(Cursor::Nothing),
            offset,
        }
    }

    pub fn is_inside_latex_curly(&self, group: &impl latex::HasCurly) -> bool {
        latex::small_range(group).contains(self.offset) || group.right_curly().is_none()
    }

    pub fn find_citation_key_word(&self) -> Option<(String, TextRange)> {
        let word = self
            .cursor
            .as_latex()
            .filter(|token| token.kind() == latex::WORD)?;

        let key = latex::Key::cast(word.parent()?)?;

        let group = latex::CurlyGroupWordList::cast(key.syntax().parent()?)?;
        latex::Citation::cast(group.syntax().parent()?)?;
        Some((key.to_string(), latex::small_range(&key)))
    }

    pub fn find_citation_key_command(&self) -> Option<(String, TextRange)> {
        let command = self.cursor.as_latex()?;

        let citation = latex::Citation::cast(command.parent()?)?;
        let key = citation.key_list()?.keys().next()?;
        Some((key.to_string(), latex::small_range(&key)))
    }

    pub fn find_entry_key(&self) -> Option<(String, TextRange)> {
        let key = self
            .cursor
            .as_bibtex()
            .filter(|token| token.kind() == bibtex::NAME)?;

        bibtex::Entry::cast(key.parent()?)?;
        Some((key.to_string(), key.text_range()))
    }

    pub fn find_label_name_key(&self) -> Option<(String, TextRange)> {
        let name = self
            .cursor
            .as_latex()
            .filter(|token| token.kind() == latex::WORD)?;

        let key = latex::Key::cast(name.parent()?)?;

        if matches!(
            key.syntax().parent()?.parent()?.kind(),
            latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE
        ) {
            Some((key.to_string(), latex::small_range(&key)))
        } else {
            None
        }
    }

    pub fn find_label_name_command(&self) -> Option<(String, TextRange)> {
        let node = self.cursor.as_latex()?.parent()?;
        if let Some(label) = latex::LabelDefinition::cast(node.clone()) {
            let name = label.name()?.key()?;
            Some((name.to_string(), latex::small_range(&name)))
        } else if let Some(label) = latex::LabelReference::cast(node.clone()) {
            let name = label.name_list()?.keys().next()?;
            Some((name.to_string(), latex::small_range(&name)))
        } else if let Some(label) = latex::LabelReferenceRange::cast(node) {
            let name = label.from()?.key()?;
            Some((name.to_string(), latex::small_range(&name)))
        } else {
            None
        }
    }

    pub fn find_environment_name(&self) -> Option<(String, TextRange)> {
        let (name, range, group) = self.find_curly_group_word()?;

        if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) {
            return None;
        }

        Some((name, range))
    }

    pub fn find_curly_group_word(&self) -> Option<(String, TextRange, latex::CurlyGroupWord)> {
        let token = self.cursor.as_latex()?;
        let key = latex::Key::cast(token.parent()?);

        let group = key
            .as_ref()
            .and_then(|key| key.syntax().parent())
            .unwrap_or(token.parent()?);

        let group =
            latex::CurlyGroupWord::cast(group).filter(|group| self.is_inside_latex_curly(group))?;

        key.map(|key| (key.to_string(), latex::small_range(&key), group.clone()))
            .or_else(|| Some((String::new(), TextRange::empty(self.offset), group)))
    }

    pub fn find_curly_group_word_list(
        &self,
    ) -> Option<(String, TextRange, latex::CurlyGroupWordList)> {
        let token = self.cursor.as_latex()?;
        let key = latex::Key::cast(token.parent()?);

        let group = key
            .as_ref()
            .and_then(|key| key.syntax().parent())
            .unwrap_or(token.parent()?);

        let group = latex::CurlyGroupWordList::cast(group)
            .filter(|group| self.is_inside_latex_curly(group))?;

        key.map(|key| {
            let range = if group
                .syntax()
                .last_token()
                .filter(|tok| tok.kind() == latex::MISSING)
                .is_some()
            {
                TextRange::new(latex::small_range(&key).start(), token.text_range().end())
            } else {
                latex::small_range(&key)
            };

            (key.to_string(), range, group.clone())
        })
        .or_else(|| Some((String::new(), TextRange::empty(self.offset), group)))
    }
}

pub trait HasPosition {
    fn position(&self) -> Position;
}

impl HasPosition for CompletionParams {
    fn position(&self) -> Position {
        self.text_document_position.position
    }
}

impl HasPosition for TextDocumentPositionParams {
    fn position(&self) -> Position {
        self.position
    }
}

impl HasPosition for RenameParams {
    fn position(&self) -> Position {
        self.text_document_position.position
    }
}

impl HasPosition for ReferenceParams {
    fn position(&self) -> Position {
        self.text_document_position.position
    }
}

impl HasPosition for HoverParams {
    fn position(&self) -> Position {
        self.text_document_position_params.position
    }
}

impl HasPosition for GotoDefinitionParams {
    fn position(&self) -> Position {
        self.text_document_position_params.position
    }
}

impl HasPosition for DocumentHighlightParams {
    fn position(&self) -> Position {
        self.text_document_position_params.position
    }
}
