use crate::joiner_ext::EmptyLines;
use crate::prelude::*;
use crate::r::lists::expression_list::FormatRExpressionListOptions;
use air_r_syntax::RRoot;
use air_r_syntax::RRootFields;
use air_r_syntax::RSyntaxNode;
use biome_formatter::CstFormatContext;
use biome_formatter::write;
use biome_rowan::Direction;
use biome_rowan::SyntaxElement;
use comments::{Directive, FormatDirective, parse_comment_directive, parse_special_skip_file};
use settings::LineEnding;

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatRRoot;

impl FormatRRoot {
    fn is_suppression_directive(directive: Option<Directive>) -> bool {
        matches!(
            directive,
            Some(Directive::Format(FormatDirective::SkipFile))
        )
    }
}

impl FormatNodeRule<RRoot> for FormatRRoot {
    fn fmt_fields(&self, node: &RRoot, f: &mut RFormatter) -> FormatResult<()> {
        let RRootFields {
            bom_token,
            expressions,
            eof_token,
        } = node.as_fields();

        let options = FormatRExpressionListOptions {
            empty_lines: EmptyLines::Double,
        };

        write!(
            f,
            [
                bom_token.format(),
                expressions.format().with_options(options),
                hard_line_break(),
                format_removed(&eof_token?),
            ]
        )
    }

    fn is_suppressed(&self, node: &RRoot, f: &RFormatter) -> bool {
        let comments = f.context().comments();
        comments.mark_suppression_checked(node.syntax());

        let Some(child) = node.expressions().first() else {
            return false;
        };

        // Only consider leading comments of the first node in the file
        let mut comments = comments
            .leading_comments(child.syntax())
            .iter()
            .map(|cmt| cmt.piece().text());

        let Some(first_line) = comments.next() else {
            return false;
        };

        // The very first line is allowed to be `Generated by:`
        if Self::is_suppression_directive(
            parse_special_skip_file(first_line).or_else(|| parse_comment_directive(first_line)),
        ) {
            return true;
        }

        for comment in comments {
            if Self::is_suppression_directive(parse_comment_directive(comment)) {
                return true;
            }
        }

        false
    }

    // The default handling of suppressed nodes in `FormatNodeRule::fmt()`
    // does not work out well for `RRoot` because it doesn't format
    // trivia verbatim. So we use a custom implementation instead.
    fn fmt_suppressed(&self, node: &RRoot, f: &mut RFormatter) -> FormatResult<()> {
        let syntax = node.syntax();

        // Mark everything as visited so the formatter doesn't panic
        // thinking we've missed elements. We're going to print each element
        // manually with `dynamic_text()`.
        mark_visited(syntax, f);

        // `node.to_trimmed_string()` trims trivia so we get the raw text on the syntax
        // node instead
        let text: String = syntax.text_with_trivia().into();
        let start = syntax.text_range_with_trivia().start();

        // Printing all of `RRoot` means that all whitespace and newlines are also printed
        // as part of the `dynamic_text()` token. The formatter's printer requires that
        // all tokens have `\n` newlines, so we must normalize here, and then the printer
        // will rewrite any `\n` it sees using the user's chosen `LineEnding`. This is
        // very similar to how `FormatStringContentToken` works, where the newlines are
        // part of the string content token, so we take responsibility for normalization.
        // This is also identical to how `fmt_suppressed()`'s default works via
        // `format_suppressed_node()` and `FormatVerbatimNode`. It normalizes line endings
        // right before calling `dynamic_text()` (#507).
        let text = match line_ending::infer(&text) {
            LineEnding::Lf => text,
            LineEnding::Crlf => line_ending::normalize(text),
        };

        // Formatting with `format_suppressed_node()` does not work well
        // here because it only formats the node verbatim, not the leading
        // and trailing trivia
        dynamic_text(&text, start).fmt(f)
    }
}

// Copied from `biome_formatter::FormatVerbatimNode.fmt()`
fn mark_visited(node: &RSyntaxNode, f: &mut RFormatter) {
    for element in node.descendants_with_tokens(Direction::Next) {
        match element {
            SyntaxElement::Token(token) => f.state_mut().track_token(&token),
            SyntaxElement::Node(node) => {
                let comments = f.context().comments();
                comments.mark_suppression_checked(&node);

                for comment in comments.leading_dangling_trailing_comments(&node) {
                    comment.mark_formatted();
                }
            }
        }
    }
}
