// Copyright 2021 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/hlsl/writer/ast_raise/localize_struct_array_assignment.h"

#include <unordered_map>
#include <utility>

#include "src/tint/lang/core/type/reference.h"
#include "src/tint/lang/wgsl/ast/assignment_statement.h"
#include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
#include "src/tint/lang/wgsl/ast/traverse_expressions.h"
#include "src/tint/lang/wgsl/program/clone_context.h"
#include "src/tint/lang/wgsl/program/program_builder.h"
#include "src/tint/lang/wgsl/resolver/resolve.h"
#include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
#include "src/tint/lang/wgsl/sem/statement.h"
#include "src/tint/lang/wgsl/sem/value_expression.h"
#include "src/tint/lang/wgsl/sem/variable.h"
#include "src/tint/utils/macros/scoped_assignment.h"

TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::LocalizeStructArrayAssignment);

namespace tint::hlsl::writer {

/// PIMPL state for the transform
struct LocalizeStructArrayAssignment::State {
    /// Constructor
    /// @param program the source program
    explicit State(const Program& program) : src(program) {}

    /// Runs the transform
    /// @returns the new program or SkipTransform if the transform is not required
    ApplyResult Run() {
        struct Shared {
            bool process_nested_nodes = false;
            Vector<const ast::Statement*, 4> insert_before_stmts;
            Vector<const ast::Statement*, 4> insert_after_stmts;
        } s;

        bool made_changes = false;

        for (auto* node : src.ASTNodes().Objects()) {
            if (auto* assign_stmt = node->As<ast::AssignmentStatement>()) {
                // Process if it's an assignment statement to a dynamically indexed array
                // within a struct on a function or private storage variable. This
                // specific use-case is what FXC fails to compile with:
                // error X3500: array reference cannot be used as an l-value; not natively
                // addressable
                if (!ContainsStructArrayIndex(assign_stmt->lhs)) {
                    continue;
                }
                auto og = GetOriginatingTypeAndAddressSpace(assign_stmt);
                if (!(og.first->Is<core::type::Struct>() &&
                      (og.second == core::AddressSpace::kFunction ||
                       og.second == core::AddressSpace::kPrivate))) {
                    continue;
                }

                ctx.Replace(assign_stmt, [&, assign_stmt] {
                    // Reset shared state for this assignment statement
                    s = Shared{};

                    const ast::Expression* new_lhs = nullptr;
                    {
                        TINT_SCOPED_ASSIGNMENT(s.process_nested_nodes, true);
                        new_lhs = ctx.Clone(assign_stmt->lhs);
                    }

                    auto* new_assign_stmt = b.Assign(new_lhs, ctx.Clone(assign_stmt->rhs));

                    // Combine insert_before_stmts + new_assign_stmt + insert_after_stmts into
                    // a block and return it
                    auto stmts = std::move(s.insert_before_stmts);
                    stmts.Reserve(1 + s.insert_after_stmts.Length());
                    stmts.Push(new_assign_stmt);
                    for (auto* stmt : s.insert_after_stmts) {
                        stmts.Push(stmt);
                    }

                    return b.Block(std::move(stmts));
                });

                made_changes = true;
            }
        }

        if (!made_changes) {
            return SkipTransform;
        }

        ctx.ReplaceAll(
            [&](const ast::IndexAccessorExpression* index_access) -> const ast::Expression* {
                if (!s.process_nested_nodes) {
                    return nullptr;
                }

                // Indexing a member access expr?
                auto* mem_access = index_access->object->As<ast::MemberAccessorExpression>();
                if (!mem_access) {
                    return nullptr;
                }

                // Process any nested IndexAccessorExpressions
                mem_access = ctx.Clone(mem_access);

                // Store the address of the member access into a let as we need to read
                // the value twice e.g. let tint_symbol = &(s.a1);
                auto mem_access_ptr = b.Sym();
                s.insert_before_stmts.Push(b.Decl(b.Let(mem_access_ptr, b.AddressOf(mem_access))));

                // Disable further transforms when cloning
                TINT_SCOPED_ASSIGNMENT(s.process_nested_nodes, false);

                // Copy entire array out of struct into local temp var
                // e.g. var tint_symbol_1 = *(tint_symbol);
                auto tmp_var = b.Sym();
                s.insert_before_stmts.Push(b.Decl(b.Var(tmp_var, b.Deref(mem_access_ptr))));

                // Replace input index_access with a clone of itself, but with its
                // .object replaced by the new temp var. This is returned from this
                // function to modify the original assignment statement. e.g.
                // tint_symbol_1[uniforms.i]
                auto* new_index_access = b.IndexAccessor(tmp_var, ctx.Clone(index_access->index));

                // Assign temp var back to array
                // e.g. *(tint_symbol) = tint_symbol_1;
                auto* assign_rhs_to_temp = b.Assign(b.Deref(mem_access_ptr), tmp_var);
                {
                    Vector<const ast::Statement*, 8> stmts{assign_rhs_to_temp};
                    for (auto* stmt : s.insert_after_stmts) {
                        stmts.Push(stmt);
                    }
                    s.insert_after_stmts = std::move(stmts);
                }

                return new_index_access;
            });

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

  private:
    /// The source program
    const Program& src;
    /// The target program builder
    ProgramBuilder b;
    /// The clone context
    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};

    /// Returns true if `expr` contains an index accessor expression to a
    /// structure member of array type.
    bool ContainsStructArrayIndex(const ast::Expression* expr) {
        bool result = false;
        TraverseExpressions(expr, [&](const ast::IndexAccessorExpression* ia) {
            // Indexing using a runtime value?
            auto* idx_sem = src.Sem().GetVal(ia->index);
            if (!idx_sem->ConstantValue()) {
                // Indexing a member access expr?
                if (auto* ma = ia->object->As<ast::MemberAccessorExpression>()) {
                    const auto* ma_ty = src.TypeOf(ma);
                    if (TINT_UNLIKELY(ma_ty->Is<core::type::Pointer>())) {
                        TINT_ICE()
                            << "lhs of index accessor expression should not be a pointer. These "
                               "should have been removed by the SimplifyPointers transform";
                        return ast::TraverseAction::Stop;
                    }
                    // That accesses an array?
                    if (ma_ty->UnwrapRef()->Is<core::type::Array>()) {
                        result = true;
                        return ast::TraverseAction::Stop;
                    }
                }
            }
            return ast::TraverseAction::Descend;
        });

        return result;
    }

    // Returns the type and address space of the originating variable of the lhs
    // of the assignment statement.
    // See https://www.w3.org/TR/WGSL/#originating-variable-section
    std::pair<const core::type::Type*, core::AddressSpace> GetOriginatingTypeAndAddressSpace(
        const ast::AssignmentStatement* assign_stmt) {
        auto* root_ident = src.Sem().GetVal(assign_stmt->lhs)->RootIdentifier();
        if (TINT_UNLIKELY(!root_ident)) {
            TINT_ICE() << "Unable to determine originating variable for lhs of assignment "
                          "statement";
            return {};
        }

        return Switch(
            root_ident->Type(),  //
            [&](const core::type::Reference* ref) {
                return std::make_pair(ref->StoreType(), ref->AddressSpace());
            },
            [&](const core::type::Pointer* ptr) {
                return std::make_pair(ptr->StoreType(), ptr->AddressSpace());
            },  //
            TINT_ICE_ON_NO_MATCH);
    }
};

LocalizeStructArrayAssignment::LocalizeStructArrayAssignment() = default;

LocalizeStructArrayAssignment::~LocalizeStructArrayAssignment() = default;

ast::transform::Transform::ApplyResult LocalizeStructArrayAssignment::Apply(
    const Program& src,
    const ast::transform::DataMap&,
    ast::transform::DataMap&) const {
    return State{src}.Run();
}

}  // namespace tint::hlsl::writer
