// Copyright 2023 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "src/tint/lang/msl/writer/ast_raise/pixel_local.h"

#include <utility>

#include "src/tint/lang/core/fluent_types.h"
#include "src/tint/lang/wgsl/program/clone_context.h"
#include "src/tint/lang/wgsl/resolver/resolve.h"
#include "src/tint/lang/wgsl/sem/function.h"
#include "src/tint/lang/wgsl/sem/module.h"
#include "src/tint/lang/wgsl/sem/statement.h"
#include "src/tint/lang/wgsl/sem/struct.h"
#include "src/tint/utils/containers/transform.h"

TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal);
TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal::Config);

using namespace tint::core::number_suffixes;  // NOLINT
using namespace tint::core::fluent_types;     // NOLINT

namespace tint::msl::writer {

/// PIMPL state for the transform
struct PixelLocal::State {
    /// The source program
    const Program& src;
    /// The target program builder
    ProgramBuilder b;
    /// The clone context
    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
    /// The transform config
    const Config& cfg;

    /// Constructor
    /// @param program the source program
    /// @param config the transform config
    State(const Program& program, const Config& config) : src(program), cfg(config) {}

    /// Runs the transform
    /// @returns the new program or SkipTransform if the transform is not required
    ApplyResult Run() {
        auto& sem = src.Sem();

        // If the pixel local extension isn't enabled, then there must be no use of pixel_local
        // variables, and so there's nothing for this transform to do.
        if (!sem.Module()->Extensions().Contains(
                wgsl::Extension::kChromiumExperimentalPixelLocal)) {
            return SkipTransform;
        }

        bool made_changes = false;

        // Change all module scope `var<pixel_local>` variables to `var<private>`.
        // We need to do this even if the variable is not referenced by the entry point as later
        // stages do not understand the pixel_local address space.
        for (auto* global : src.AST().GlobalVariables()) {
            if (auto* var = global->As<ast::Var>()) {
                if (sem.Get(var)->AddressSpace() == core::AddressSpace::kPixelLocal) {
                    // Change the 'var<pixel_local>' to 'var<private>'
                    ctx.Replace(var->declared_address_space, b.Expr(core::AddressSpace::kPrivate));
                    made_changes = true;
                }
            }
        }

        // Find the single entry point
        const sem::Function* entry_point = nullptr;
        for (auto* fn : src.AST().Functions()) {
            if (fn->IsEntryPoint()) {
                if (entry_point != nullptr) {
                    TINT_ICE() << "PixelLocal transform requires that the SingleEntryPoint "
                                  "transform has already been run";
                    return SkipTransform;
                }
                entry_point = sem.Get(fn);

                // Look for a `var<pixel_local>` used by the entry point...
                for (auto* global : entry_point->TransitivelyReferencedGlobals()) {
                    if (global->AddressSpace() != core::AddressSpace::kPixelLocal) {
                        continue;
                    }

                    // Obtain struct of the pixel local.
                    auto* pixel_local_str = global->Type()->UnwrapRef()->As<sem::Struct>();

                    // Add an Color attribute to each member of the pixel_local structure.
                    for (auto* member : pixel_local_str->Members()) {
                        ctx.InsertBack(member->Declaration()->attributes,
                                       b.Color(u32(AttachmentIndex(member->Index()))));
                        ctx.InsertBack(member->Declaration()->attributes,
                                       b.Disable(ast::DisabledValidation::kEntryPointParameter));
                    }

                    TransformEntryPoint(entry_point, global, pixel_local_str);
                    made_changes = true;

                    break;  // Only a single `var<pixel_local>` can be used by an entry point.
                }
            }
        }

        if (!made_changes) {
            return SkipTransform;
        }

        // At this point, the `var<pixel_local>` will have been replaced with `var<private>`, and
        // the entry point will use `@color`, which requires the framebuffer fetch extension.
        // Replace the `chromium_experimental_pixel_local` enable with
        // `chromium_experimental_framebuffer_fetch`.
        for (auto* enable : src.AST().Enables()) {
            for (auto* ext : enable->extensions) {
                if (ext->name == wgsl::Extension::kChromiumExperimentalPixelLocal) {
                    ctx.Replace(ext, b.create<ast::Extension>(
                                         wgsl::Extension::kChromiumExperimentalFramebufferFetch));
                }
            }
        }

        ctx.Clone();
        return resolver::Resolve(b);
    }

    /// Transforms the entry point @p entry_point to handle the direct or transitive usage of the
    /// `var<pixel_local>` @p pixel_local_var.
    /// @param entry_point the entry point
    /// @param pixel_local_var the `var<pixel_local>`
    /// @param pixel_local_str the struct type of the var
    void TransformEntryPoint(const sem::Function* entry_point,
                             const sem::GlobalVariable* pixel_local_var,
                             const sem::Struct* pixel_local_str) {
        auto* fn = entry_point->Declaration();
        auto fn_name = fn->name->symbol.Name();
        auto pixel_local_str_name = ctx.Clone(pixel_local_str->Name());
        auto pixel_local_var_name = ctx.Clone(pixel_local_var->Declaration()->name->symbol);

        // Remove the @fragment attribute from the entry point
        ctx.Remove(fn->attributes, ast::GetAttribute<ast::StageAttribute>(fn->attributes));
        // Rename the entry point
        auto inner_name = b.Symbols().New(fn_name + "_inner");
        ctx.Replace(fn->name, b.Ident(inner_name));

        // Create a new function that wraps the entry point.
        // This function has all the existing entry point parameters and an additional
        // parameter for the input pixel local structure.
        auto params = ctx.Clone(fn->params);
        auto pl_param = b.Symbols().New("pixel_local");
        params.Push(b.Param(pl_param, b.ty(pixel_local_str_name)));

        // Remove any entry-point attributes from the inner function.
        // This must come after `ctx.Clone(fn->params)` as we want these attributes on the outer
        // function.
        for (auto* param : fn->params) {
            for (auto* attr : param->attributes) {
                if (attr->IsAnyOf<ast::BuiltinAttribute, ast::LocationAttribute,
                                  ast::InterpolateAttribute, ast::InvariantAttribute>()) {
                    ctx.Remove(param->attributes, attr);
                }
            }
        }

        // Build the outer function's statements, starting with an assignment of the pixel local
        // parameter to the module scope var.
        Vector<const ast::Statement*, 3> body{
            b.Assign(pixel_local_var_name, pl_param),
        };

        // Build the arguments to call the inner function
        auto call_args =
            tint::Transform(fn->params, [&](auto* p) { return b.Expr(ctx.Clone(p->name)); });

        // Create a structure to hold the combined flattened result of the entry point and the pixel
        // local structure.
        auto str_name = b.Symbols().New(fn_name + "_res");
        Vector<const ast::StructMember*, 8> members;
        Vector<const ast::Expression*, 8> return_args;  // arguments to the final `return` statement

        auto add_member = [&](const core::type::Type* ty, VectorRef<const ast::Attribute*> attrs) {
            members.Push(b.Member("output_" + std::to_string(members.Length()),
                                  CreateASTTypeFor(ctx, ty), std::move(attrs)));
        };
        for (auto* member : pixel_local_str->Members()) {
            add_member(member->Type(), Vector{
                                           b.Location(AInt(AttachmentIndex(member->Index()))),
                                       });
            return_args.Push(b.MemberAccessor(pixel_local_var_name, ctx.Clone(member->Name())));
        }
        if (fn->return_type) {
            Symbol call_result = b.Symbols().New("result");
            if (auto* str = entry_point->ReturnType()->As<sem::Struct>()) {
                // The entry point returned a structure.
                for (auto* member : str->Members()) {
                    auto& member_attrs = member->Declaration()->attributes;
                    add_member(member->Type(), ctx.Clone(member_attrs));
                    return_args.Push(b.MemberAccessor(call_result, ctx.Clone(member->Name())));
                    if (auto* location = ast::GetAttribute<ast::LocationAttribute>(member_attrs)) {
                        // Remove the @location attribute from the member of the inner function's
                        // output structure.
                        // Note: This will break other entry points that share the same output
                        // structure, however this transform assumes that the SingleEntryPoint
                        // transform will have already been run.
                        ctx.Remove(member_attrs, location);
                    }
                }
            } else {
                // The entry point returned a non-structure
                add_member(entry_point->ReturnType(), ctx.Clone(fn->return_type_attributes));
                return_args.Push(b.Expr(call_result));

                // Remove the @location from the inner function's return type attributes
                ctx.Remove(fn->return_type_attributes,
                           ast::GetAttribute<ast::LocationAttribute>(fn->return_type_attributes));
            }
            body.Push(b.Decl(b.Let(call_result, b.Call(inner_name, std::move(call_args)))));
        } else {
            body.Push(b.CallStmt(b.Call(inner_name, std::move(call_args))));
        }

        // Declare the output structure
        b.Structure(str_name, std::move(members));

        // Return the output structure
        body.Push(b.Return(b.Call(str_name, std::move(return_args))));

        // Declare the new entry point that calls the inner function
        b.Func(fn_name, std::move(params), b.ty(str_name), body,
               Vector{b.Stage(ast::PipelineStage::kFragment)});
    }

    /// @returns the attachment index for the pixel local field with the given index
    /// @param field_index the pixel local field index
    uint32_t AttachmentIndex(uint32_t field_index) {
        auto idx = cfg.attachments.Get(field_index);
        if (TINT_UNLIKELY(!idx)) {
            b.Diagnostics().add_error(diag::System::Transform,
                                      "PixelLocal::Config::attachments missing entry for field " +
                                          std::to_string(field_index));
            return 0;
        }
        return *idx;
    }
};

PixelLocal::PixelLocal() = default;

PixelLocal::~PixelLocal() = default;

ast::transform::Transform::ApplyResult PixelLocal::Apply(const Program& src,
                                                         const ast::transform::DataMap& inputs,
                                                         ast::transform::DataMap&) const {
    auto* cfg = inputs.Get<Config>();
    if (!cfg) {
        ProgramBuilder b;
        b.Diagnostics().add_error(diag::System::Transform,
                                  "missing transform data for " + std::string(TypeInfo().name));
        return resolver::Resolve(b);
    }

    return State(src, *cfg).Run();
}

PixelLocal::Config::Config() = default;

PixelLocal::Config::Config(const Config&) = default;

PixelLocal::Config::~Config() = default;

}  // namespace tint::msl::writer
