use lsp_types::{DocumentFormattingParams, TextEdit};
use rowan::{ast::AstNode, NodeOrToken};

use crate::{
    features::FeatureRequest,
    syntax::bibtex::{self, HasName, HasType, HasValue},
    LineIndex, LineIndexExt,
};

pub fn format_bibtex_internal(
    request: &FeatureRequest<DocumentFormattingParams>,
) -> Option<Vec<TextEdit>> {
    let mut indent = String::new();
    if request.params.options.insert_spaces {
        for _ in 0..request.params.options.tab_size {
            indent.push(' ');
        }
    } else {
        indent.push('\t');
    }

    let line_length = request
        .workspace
        .environment
        .options
        .formatter_line_length
        .map(|value| {
            if value <= 0 {
                usize::MAX
            } else {
                value as usize
            }
        })
        .unwrap_or(80);

    let document = request.main_document();
    let data = document.data.as_bibtex()?;
    let mut edits = Vec::new();

    for node in bibtex::SyntaxNode::new_root(data.green.clone())
        .children()
        .filter(|node| {
            matches!(
                node.kind(),
                bibtex::PREAMBLE | bibtex::STRING | bibtex::ENTRY
            )
        })
    {
        let range = node.text_range();

        let mut formatter = Formatter::new(
            indent.clone(),
            request.params.options.tab_size,
            line_length,
            &document.line_index,
        );

        formatter.visit_node(node);
        edits.push(TextEdit {
            range: document.line_index.line_col_lsp_range(range),
            new_text: formatter.output,
        });
    }

    Some(edits)
}

struct Formatter<'a> {
    indent: String,
    tab_size: u32,
    line_length: usize,
    output: String,
    align: Vec<usize>,
    line_index: &'a LineIndex,
}

impl<'a> Formatter<'a> {
    fn new(indent: String, tab_size: u32, line_length: usize, line_index: &'a LineIndex) -> Self {
        Self {
            indent,
            tab_size,
            line_length,
            output: String::new(),
            align: Vec::new(),
            line_index,
        }
    }

    fn visit_token_lowercase(&mut self, token: &bibtex::SyntaxToken) {
        self.output.push_str(&token.text().to_lowercase());
    }

    fn should_insert_space(
        &self,
        previous: &bibtex::SyntaxToken,
        current: &bibtex::SyntaxToken,
    ) -> bool {
        let previous_range = self.line_index.line_col_lsp_range(previous.text_range());
        let current_range = self.line_index.line_col_lsp_range(current.text_range());
        previous_range.start.line != current_range.start.line
            || previous_range.end.character < current_range.start.character
    }

    fn base_align(&self) -> usize {
        self.output[self.output.rfind('\n').unwrap_or(0)..]
            .chars()
            .count()
    }

    fn visit_node(&mut self, parent: bibtex::SyntaxNode) {
        match parent.kind() {
            bibtex::PREAMBLE => {
                let preamble = bibtex::Preamble::cast(parent).unwrap();
                self.visit_token_lowercase(&preamble.type_token().unwrap());
                self.output.push('{');
                if preamble.syntax().children().next().is_some() {
                    self.align.push(self.base_align());
                    for node in preamble.syntax().children() {
                        self.visit_node(node);
                    }
                    self.output.push('}');
                }
            }
            bibtex::STRING => {
                let string = bibtex::StringDef::cast(parent).unwrap();
                self.visit_token_lowercase(&string.type_token().unwrap());
                self.output.push('{');
                if let Some(name) = string.name_token() {
                    self.output.push_str(name.text());
                    self.output.push_str(" = ");
                    if let Some(value) = string.value() {
                        self.align.push(self.base_align());
                        self.visit_node(value.syntax().clone());
                        self.output.push('}');
                    }
                }
            }
            bibtex::ENTRY => {
                let entry = bibtex::Entry::cast(parent).unwrap();
                self.visit_token_lowercase(&entry.type_token().unwrap());
                self.output.push('{');
                if let Some(key) = entry.name_token() {
                    self.output.push_str(&key.to_string());
                    self.output.push(',');
                    self.output.push('\n');
                    for field in entry.fields() {
                        self.visit_node(field.syntax().clone());
                    }
                    self.output.push('}');
                }
            }
            bibtex::FIELD => {
                let field = bibtex::Field::cast(parent).unwrap();
                self.output.push_str(&self.indent);
                let name = field.name_token().unwrap();
                self.output.push_str(name.text());
                self.output.push_str(" = ");
                if let Some(value) = field.value() {
                    let count = name.text().chars().count();
                    self.align.push(self.tab_size as usize + count + 3);
                    self.visit_node(value.syntax().clone());
                    self.output.push(',');
                    self.output.push('\n');
                }
            }
            kind if bibtex::Value::can_cast(kind) => {
                let tokens: Vec<_> = parent
                    .descendants_with_tokens()
                    .filter_map(|element| element.into_token())
                    .filter(|token| token.kind() != bibtex::WHITESPACE)
                    .collect();

                self.output.push_str(tokens[0].text());

                let align = self.align.pop().unwrap_or_default();
                let mut length = align + tokens[0].text().chars().count();
                for i in 1..tokens.len() {
                    let previous = &tokens[i - 1];
                    let current = &tokens[i];
                    let current_length = current.text().chars().count();

                    let insert_space = self.should_insert_space(previous, current);
                    let space_length = if insert_space { 1 } else { 0 };

                    if length + current_length + space_length > self.line_length {
                        self.output.push('\n');
                        self.output.push_str(self.indent.as_ref());
                        for _ in 0..=align - self.tab_size as usize {
                            self.output.push(' ');
                        }
                        length = align;
                    } else if insert_space {
                        self.output.push(' ');
                        length += 1;
                    }
                    self.output.push_str(current.text());
                    length += current_length;
                }
            }
            bibtex::ROOT | bibtex::JUNK => {
                for element in parent.children_with_tokens() {
                    match element {
                        NodeOrToken::Token(token) => {
                            self.output.push_str(token.text());
                        }
                        NodeOrToken::Node(node) => {
                            self.visit_node(node);
                        }
                    }
                }
            }
            _ => unreachable!(),
        }
    }
}
