use anyhow::Context;
use lsp_types::{self as types, DocumentRangeFormattingRequest, Range};

use crate::edit::{RangeExt, ToRangeExt};
use crate::resolve::is_document_excluded_for_formatting;
use crate::server::Result;
use crate::server::api::LSPResult;
use crate::session::{Client, DocumentQuery, DocumentSnapshot};
use crate::{PositionEncoding, TextDocument};

pub(crate) struct FormatRange;

impl super::RequestHandler for FormatRange {
    type RequestType = DocumentRangeFormattingRequest;
}

impl super::BackgroundDocumentRequestHandler for FormatRange {
    super::define_document_uri!(params: &types::DocumentRangeFormattingParams);

    fn run_with_snapshot(
        snapshot: Self::Snapshot,
        _client: &Client,
        params: types::DocumentRangeFormattingParams,
    ) -> Result<super::FormatResponse> {
        let snapshot = match snapshot {
            Ok(snapshot) => snapshot,
            Err(uri) => {
                tracing::warn!(
                    "Returning no range formatting edits because document `{uri}` isn't open."
                );
                return Ok(None);
            }
        };

        format_document_range(&snapshot, params.range)
    }
}

/// Formats the specified [`Range`] in the [`DocumentSnapshot`].
fn format_document_range(
    snapshot: &DocumentSnapshot,
    range: Range,
) -> Result<super::FormatResponse> {
    let text_document = snapshot
        .query()
        .as_single_document()
        .context("Failed to get text document for the format range request")
        .unwrap();
    let query = snapshot.query();
    let backend = snapshot
        .client_settings()
        .editor_settings()
        .format_backend();
    format_text_document_range(text_document, range, query, snapshot.encoding(), backend)
}

/// Formats the specified [`Range`] in the [`TextDocument`].
fn format_text_document_range(
    text_document: &TextDocument,
    range: Range,
    query: &DocumentQuery,
    encoding: PositionEncoding,
    backend: crate::format::FormatBackend,
) -> Result<super::FormatResponse> {
    let settings = query.settings();
    let file_path = query.virtual_file_path();
    let source_type = query.source_type_for_format();

    // If the document is excluded, return early.
    if is_document_excluded_for_formatting(
        &file_path,
        &settings.file_resolver,
        &settings.formatter,
        text_document.language_id(),
    ) {
        return Ok(None);
    }

    let text = text_document.contents();
    let index = text_document.index();
    let range = range.to_text_range(text, index, encoding);
    let formatted_range = crate::format::format_range(
        text_document,
        source_type,
        &settings.formatter,
        range,
        &file_path,
        backend,
    )
    .with_failure_code(lsp_server::ErrorCode::InternalError)?;

    Ok(formatted_range.map(|formatted_range| {
        vec![types::TextEdit {
            range: formatted_range
                .source_range()
                .to_range(text, index, encoding),
            new_text: formatted_range.into_code(),
        }]
    }))
}
