//! Generator for `oxc_formatter`.
//!

use proc_macro2::TokenStream;
use quote::{format_ident, quote};

use crate::{
    Codegen, Generator,
    generators::{define_generator, formatter::ast_nodes::get_node_type},
    output::Output,
    schema::{Def, EnumDef, Schema, StructDef, StructOrEnum, TypeDef, TypeId},
};

use super::ast_nodes::formatter_output_path;

/// Based on the prettier printing comments algorithm, these nodes don't need to print comments.
const AST_NODE_WITHOUT_PRINTING_COMMENTS_LIST: &[&str] = &[
    "Program",
    "FormalParameters",
    "FunctionBody",
    "ClassBody",
    "CatchParameter",
    "CatchClause",
    // Manually prints it because class's decorators can be appears before `export class Cls {}`.
    "ExportNamedDeclaration",
    "ExportDefaultDeclaration",
    //
    "JSXElement",
    "JSXFragment",
    //
    "TemplateElement",
];

// `ExpressionStatement` prints leading comments in its `write` implementation,
// so the ASI-guard semicolon can be printed before a leading type cast comment.
const AST_NODE_WITHOUT_PRINTING_LEADING_COMMENTS_LIST: &[&str] =
    &["TSUnionType", "ExpressionStatement"];

// Statements whose suppressed (`oxfmt-ignore`d) range must exclude the trailing semicolon,
// so the formatter prints its own terminator like Prettier's ignored range.
// Every node listed here MUST implement `FormatWrite::write_suppressed`
// (the default implementation panics at runtime).
//
// Mirrors Prettier's `locEnd` overrides (`language-js/location/overrides.js`) +
// `shouldIgnoredNodePrintSemicolon`: keyword statements end at their keyword/label,
// content-terminated ones at their content, body-ended ones recurse into the rightmost body.
//
// `ExpressionStatement` and `VariableDeclaration` have the same issue in principle
// but no confirmed divergence against Prettier 3.9 yet.
//
// Extend the list one statement at a time, verifying each against Prettier first.
const AST_NODE_WITH_CUSTOM_SUPPRESSED_FORMATTING: &[&str] = &[
    "BreakStatement",
    "ContinueStatement",
    "DebuggerStatement",
    "DoWhileStatement",
    "ForInStatement",
    "ForOfStatement",
    "ForStatement",
    "IfStatement",
    "LabeledStatement",
    "ReturnStatement",
    "ThrowStatement",
    "WhileStatement",
    "WithStatement",
];

const AST_NODE_NEEDS_PARENTHESES: &[&str] = &[
    "TSTypeAssertion",
    "TSInferType",
    "TSConditionalType",
    "TSUnionType",
    "TSIntersectionType",
    "TSConstructorType",
    "TSTypeQuery",
    "TSFunctionType",
    "TSTypeOperator",
];

const NEEDS_IMPLEMENTING_FMT_WITH_OPTIONS: phf::Map<&'static str, &'static str> = phf::phf_map! {
    "ArrowFunctionExpression" => "FormatJsArrowFunctionExpressionOptions",
    "Function" => "FormatFunctionOptions",
};

pub struct FormatterFormatGenerator;

define_generator!(FormatterFormatGenerator);

impl Generator for FormatterFormatGenerator {
    fn generate(&self, schema: &Schema, _codegen: &Codegen) -> Output {
        let parenthesis_type_ids = get_needs_parentheses_type_ids(schema);

        let impls = schema
            .structs_and_enums()
            .filter_map(|type_def| match type_def {
                StructOrEnum::Struct(struct_def)
                    if struct_def.visit.has_visitor() && !struct_def.builder.skip =>
                {
                    Some(generate_struct_implementation(struct_def, &parenthesis_type_ids, schema))
                }
                StructOrEnum::Enum(enum_def) if enum_def.visit.has_visitor() => {
                    Some(generate_enum_implementation(enum_def, schema))
                }
                _ => None,
            })
            .collect::<TokenStream>();

        let options = NEEDS_IMPLEMENTING_FMT_WITH_OPTIONS.values().map(|o| {
            let ident = format_ident!("{}", o);
            quote! { , #ident }
        });

        let output = quote! {
            #![expect(clippy::match_same_arms)]
            use oxc_ast::ast::*;
            use oxc_span::GetSpan;

            ///@@line_break
            use crate::{
                formatter::{Format, JsFormatContext, JsFormatter, JsFormatterExt as _, trivia::{format_leading_comments, format_trailing_comments}},
                parentheses::NeedsParentheses,
                ast_nodes::AstNode,
                utils::{suppressed::FormatSuppressedNode, typecast::{format_type_cast_comment_node, format_leading_comments_and_open_paren}},
                print::{FormatWrite #(#options)*},
            };

            #impls
        };

        Output::Rust { path: formatter_output_path("format"), tokens: output }
    }
}

fn generate_struct_implementation(
    struct_def: &StructDef,
    parenthesis_type_ids: &[TypeId],
    schema: &Schema,
) -> TokenStream {
    let type_ty = struct_def.ty(schema);
    let type_ty = quote! {
        AstNode::<'a, #type_ty>
    };

    let struct_name = struct_def.name();
    let do_not_print_comment = AST_NODE_WITHOUT_PRINTING_COMMENTS_LIST.contains(&struct_name);
    let do_not_print_leading_comment = do_not_print_comment
        || AST_NODE_WITHOUT_PRINTING_LEADING_COMMENTS_LIST.contains(&struct_name);

    let needs_parentheses = parenthesis_type_ids.contains(&struct_def.id);

    // For nodes that may get formatter-added parentheses,
    // leading comments are printed by the parentheses block below (ordering depends on the comments),
    // not as a standalone step.
    let leading_comments = (!do_not_print_leading_comment && !needs_parentheses).then(|| {
        quote! {
            self.format_leading_comments(f);
        }
    });
    let trailing_comments = (!do_not_print_comment).then(|| {
        quote! {
            self.format_trailing_comments(f);
        }
    });

    let needs_parentheses_before = if needs_parentheses {
        if do_not_print_leading_comment {
            quote! {
                let needs_parentheses = self.needs_parentheses(f);
                if needs_parentheses {
                    "(".fmt(f);
                }
            }
        } else {
            // A leading type cast comment must stay adjacent to the `(` of its cast target inside this node;
            // the helper prints the comments inside the added parentheses in that case,
            // or the cast would rebind to them.
            quote! {
                let needs_parentheses = self.needs_parentheses(f);
                format_leading_comments_and_open_paren(self.span(), needs_parentheses, f);
            }
        }
    } else {
        quote! {}
    };

    let needs_parentheses_after = if needs_parentheses {
        quote! {
            if needs_parentheses {
                ")".fmt(f);
            }
        }
    } else {
        quote! {}
    };

    let generate_fmt_implementation = |has_options: bool| {
        let write_call = if has_options {
            quote! {
                self.write_with_options(options, f);
            }
        } else {
            quote! {
                self.write(f);
            }
        };

        // `Program` can't be suppressed.
        // `JSXElement` and `JSXFragment` implement suppression formatting in their formatting logic
        let suppressed_check = (!matches!(struct_name, "Program" | "JSXElement" | "JSXFragment"))
            .then(|| {
                quote! {
                    let is_suppressed = f.comments().is_suppressed(self.span().start);
                }
            });

        let write_implementation = if suppressed_check.is_none() {
            write_call
        } else {
            let suppressed_write =
                if AST_NODE_WITH_CUSTOM_SUPPRESSED_FORMATTING.contains(&struct_name) {
                    quote! { self.write_suppressed(f); }
                } else {
                    quote! { FormatSuppressedNode(self.span()).fmt(f); }
                };
            // When `fmt` doesn't print leading/trailing comments itself,
            // the suppressed path still has to print them, or the suppression comment would be lost.
            let suppressed_leading_comments = do_not_print_leading_comment.then(|| {
                quote! {
                    self.format_leading_comments(f);
                }
            });
            let suppressed_trailing_comments = do_not_print_comment.then(|| {
                quote! {
                    self.format_trailing_comments(f);
                }
            });
            quote! {
                if is_suppressed {
                    #suppressed_leading_comments
                    #suppressed_write
                    #suppressed_trailing_comments
                } else {
                    #write_call
                }
            }
        };

        let type_cast_comment_formatting = needs_parentheses.then(|| {
            let is_object_or_array_argument =
                if matches!(struct_def.name.as_str(), "ObjectExpression" | "ArrayExpression") {
                    quote! {
                        true
                    }
                } else {
                    quote! { false }
                };

            let suppressed_check_for_typecast = suppressed_check.is_some().then(|| {
                quote! {
                    !is_suppressed &&
                }
            });

            quote! {
                if #suppressed_check_for_typecast format_type_cast_comment_node(self, #is_object_or_array_argument, f) {
                    return;
                }
            }
        });

        if !needs_parentheses && trailing_comments.is_none() {
            quote! {
                #suppressed_check
                #type_cast_comment_formatting
                #write_implementation
            }
        } else {
            quote! {
                #suppressed_check
                #type_cast_comment_formatting
                #leading_comments
                #needs_parentheses_before
                #write_implementation
                #needs_parentheses_after
                #trailing_comments
            }
        }
    };

    let fmt_implementation = generate_fmt_implementation(false);
    let fmt_options =
        NEEDS_IMPLEMENTING_FMT_WITH_OPTIONS.get(struct_name).map(|str| format_ident!("{}", str));
    let fmt_with_options_inherent = if let Some(ref fmt_options) = fmt_options {
        let implementation = generate_fmt_implementation(true);
        quote! {
            ///@@line_break
            impl<'a> #type_ty {
                pub fn fmt_with_options(&self, options: #fmt_options, f: &mut JsFormatter<'_, 'a>) {
                    #implementation
                }
            }
        }
    } else {
        quote! {}
    };

    quote! {
        ///@@line_break
        impl<'a> Format<'a, JsFormatContext<'a>> for #type_ty {
            fn fmt(&self, f: &mut JsFormatter<'_, 'a>) {
                #fmt_implementation
            }
        }

        #fmt_with_options_inherent
    }
}

fn generate_enum_implementation(enum_def: &EnumDef, schema: &Schema) -> TokenStream {
    let enum_ident = enum_def.ident();
    let enum_ty = enum_def.ty(schema);

    let variant_match_arms = enum_def.variants.iter().map(|variant| {
        let variant_name = &variant.ident();
        let field_type = variant.field_type(schema).unwrap();
        let node_type =
            field_type.maybe_inner_type(schema).map_or_else(|| field_type.ident(), TypeDef::ident);

        Some(quote! {
            #enum_ident::#variant_name(inner) => {
                allocator.alloc(AstNode::<#node_type> {
                    inner,
                    parent,
                    allocator,
                    following_span_start: self.following_span_start,
                }).fmt(f);
            },
        })
    });

    let inherits_match_arms = enum_def.inherits_enums(schema).map(|inherits_type| {
        let inherits_ident = inherits_type.ident();
        let inherits_snake_name = inherits_type.snake_name();
        let match_ident = format_ident!("match_{inherits_snake_name}");

        let to_fn_ident = format_ident!("to_{inherits_snake_name}");
        let match_arm = quote! {
            it @ #match_ident!(#enum_ident) => {
                let inner = it.#to_fn_ident();
                allocator.alloc(AstNode::<'a, #inherits_ident> {
                    inner,
                    parent,
                    allocator,
                    following_span_start: self.following_span_start,
                }).fmt(f);
            },
        };

        match_arm
    });

    let node_type = get_node_type(&enum_ty);

    let inline_trailing_suppression = match enum_def.name() {
        "Statement" => {
            // Expression statements need specialized ASI-safe suppression handling in
            // `AstNode<ExpressionStatement>::write`.
            quote! {
                if !matches!(self.inner, Statement::ExpressionStatement(_))
                    && f.comments().has_trailing_suppression_comment(self.span().end)
                {
                    format_leading_comments(self.span()).fmt(f);
                    FormatSuppressedNode(self.span()).fmt(f);
                    format_trailing_comments(self.parent.span(), self.inner.span(), self.following_span_start)
                        .fmt(f);
                    return;
                }
            }
        }
        "Expression" => {
            quote! {
                if f.comments().has_trailing_suppression_comment(self.span().end) {
                    format_leading_comments(self.span()).fmt(f);
                    FormatSuppressedNode(self.span()).fmt(f);
                    format_trailing_comments(self.parent.span(), self.inner.span(), self.following_span_start)
                        .fmt(f);
                    return;
                }
            }
        }
        _ => quote! {},
    };

    quote! {
        ///@@line_break
        impl<'a> Format<'a, JsFormatContext<'a>> for #node_type {
            #[inline]
            fn fmt(&self, f: &mut JsFormatter<'_, 'a>) {
                #inline_trailing_suppression
                let allocator = self.allocator;
                let parent = self.parent;
                match self.inner {
                    #(#variant_match_arms)*
                    #(#inherits_match_arms)*
                }
            }
        }
    }
}

/// Get [`TypeId`]s of types which do not have a following node.
fn get_needs_parentheses_type_ids(schema: &Schema) -> Vec<TypeId> {
    let mut type_ids =
        AST_NODE_NEEDS_PARENTHESES.iter().map(|&name| schema.type_names[name]).collect::<Vec<_>>();

    let expression_enum = schema.type_by_name("Expression").as_enum().unwrap();
    type_ids.extend(
        expression_enum
            .all_variants(schema)
            .filter_map(|variant| variant.field_type(schema))
            .map(|variant_type| variant_type.innermost_type(schema).id()),
    );

    type_ids
}
