/**
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "destructuring.h"

#include "util/helpers.h"
#include "compiler/base/iterators.h"
#include "compiler/base/lreference.h"
#include "compiler/base/catchTable.h"
#include "compiler/core/pandagen.h"
#include "ir/base/property.h"
#include "ir/base/spreadElement.h"
#include "ir/expressions/arrayExpression.h"
#include "ir/expressions/assignmentExpression.h"
#include "ir/expressions/identifier.h"
#include "ir/expressions/objectExpression.h"

namespace panda::es2panda::compiler {
static void GenRestElement(PandaGen *pg, const ir::SpreadElement *restElement,
                           const DestructuringIterator &destIterator, bool isDeclaration)
{
    VReg array = pg->AllocReg();
    VReg index = pg->AllocReg();

    auto *next = pg->AllocLabel();
    auto *done = pg->AllocLabel();

    DestructuringRestIterator iterator(destIterator);

    // create left reference for rest element
    auto lref = JSLReference::Create(pg, restElement, isDeclaration);

    // create an empty array first
    pg->CreateEmptyArray(restElement);
    pg->StoreAccumulator(restElement, array);

    // index = 0
    pg->LoadAccumulatorInt(restElement, 0);
    pg->StoreAccumulator(restElement, index);

    pg->SetLabel(restElement, next);

    iterator.Step(done);
    pg->StoreObjByValue(restElement, array, index);

    // index++
    pg->LoadAccumulatorInt(restElement, 1);
    pg->Binary(restElement, lexer::TokenType::PUNCTUATOR_PLUS, index);
    pg->StoreAccumulator(restElement, index);

    pg->Branch(restElement, next);

    pg->SetLabel(restElement, done);
    pg->LoadAccumulator(restElement, array);

    lref.SetValue();
}

static void GenArray(PandaGen *pg, const ir::ArrayExpression *array)
{
    DestructuringIterator iterator(pg, array);

    if (array->Elements().empty()) {
        iterator.Close(false);
        return;
    }

    TryContext tryCtx(pg);
    const auto &labelSet = tryCtx.LabelSet();
    pg->SetLabel(array, labelSet.TryBegin());

    for (const auto *element : array->Elements()) {
        RegScope ers(pg);

        if (element->IsRestElement()) {
            GenRestElement(pg, element->AsRestElement(), iterator, array->IsDeclaration());
            break;
        }

        // if a hole exist, just let the iterator step ahead
        if (element->IsOmittedExpression()) {
            iterator.Step();
            continue;
        }

        const ir::Expression *init = nullptr;
        const ir::Expression *target = element;

        if (element->IsAssignmentPattern()) {
            target = element->AsAssignmentPattern()->Left();
            init = element->AsAssignmentPattern()->Right();
        }

        auto lref = JSLReference::Create(pg, target, array->IsDeclaration());
        iterator.Step();

        if (init != nullptr) {
            auto *assignValue = pg->AllocLabel();
            auto *defaultInit = pg->AllocLabel();
            pg->BranchIfUndefined(element, defaultInit);
            pg->LoadAccumulator(element, iterator.Result());
            pg->Branch(element, assignValue);

            pg->SetLabel(element, defaultInit);
            init->Compile(pg);
            pg->SetLabel(element, assignValue);
        }

        lref.SetValue();
    }

    pg->SetLabel(array, labelSet.TryEnd());

    // Normal completion
    pg->LoadAccumulator(array, iterator.Done());
    pg->BranchIfTrue(array, labelSet.CatchEnd());
    iterator.Close(false);

    pg->Branch(array, labelSet.CatchEnd());

    Label *end = pg->AllocLabel();
    pg->SetLabel(array, labelSet.CatchBegin());
    pg->StoreAccumulator(array, iterator.Result());
    pg->LoadAccumulator(array, iterator.Done());

    pg->BranchIfTrue(array, end);
    pg->LoadAccumulator(array, iterator.Result());
    iterator.Close(true);
    pg->SetLabel(array, end);
    pg->LoadAccumulator(array, iterator.Result());
    pg->EmitThrow(array);
    pg->SetLabel(array, labelSet.CatchEnd());
}

static std::tuple<const ir::Expression *, const ir::Expression *> GetAssignmentTarget(const ir::Property *propExpr)
{
    const ir::Expression *init = nullptr;
    const ir::Expression *target = propExpr->Value();

    if (target->IsAssignmentPattern()) {
        init = target->AsAssignmentPattern()->Right();
        target = target->AsAssignmentPattern()->Left();
    }

    return {init, target};
}

static void GenDefaultInitializer(PandaGen *pg, const ir::Expression *element, const ir::Expression *init)
{
    if (init == nullptr) {
        return;
    }

    RegScope rs(pg);
    VReg loadedValue = pg->AllocReg();
    pg->StoreAccumulator(element, loadedValue);

    auto *getDefault = pg->AllocLabel();
    auto *store = pg->AllocLabel();

    pg->BranchIfUndefined(element, getDefault);
    pg->LoadAccumulator(element, loadedValue);
    pg->Branch(element, store);

    // load default value
    pg->SetLabel(element, getDefault);
    init->Compile(pg);

    pg->SetLabel(element, store);
}

static void GenObjectWithRest(PandaGen *pg, const ir::ObjectExpression *object, VReg rhs)
{
    const auto &properties = object->Properties();

    RegScope rs(pg);
    VReg propStart = pg->NextReg();

    for (const auto *element : properties) {
        if (element->IsRestElement()) {
            RegScope restScope(pg);
            auto lref = JSLReference::Create(pg, element, object->IsDeclaration());
            pg->CreateObjectWithExcludedKeys(element, rhs, propStart, properties.size() - 1);
            lref.SetValue();
            break;
        }

        VReg propReg = pg->AllocReg();

        RegScope propScope(pg);

        const ir::Property *propExpr = element->AsProperty();
        const ir::Expression *key = propExpr->Key();
        const auto [init, target] = GetAssignmentTarget(propExpr);

        if (key->IsIdentifier()) {
            pg->LoadAccumulatorString(key, key->AsIdentifier()->Name());
        } else {
            key->Compile(pg);
        }

        pg->StoreAccumulator(key, propReg);

        auto lref = JSLReference::Create(pg, target, object->IsDeclaration());

        pg->LoadAccumulator(element, propReg);
        pg->LoadObjByValue(element, rhs);

        GenDefaultInitializer(pg, element, init);

        lref.SetValue();
    }
}

static void GenObject(PandaGen *pg, const ir::ObjectExpression *object, VReg rhs)
{
    const auto &properties = object->Properties();

    if (properties.empty() || properties.back()->IsRestElement()) {
        auto *notNullish = pg->AllocLabel();

        pg->LoadAccumulator(object, rhs);
        pg->BranchIfCoercible(object, notNullish);
        pg->ThrowObjectNonCoercible(object);

        pg->SetLabel(object, notNullish);

        if (!properties.empty()) {
            return GenObjectWithRest(pg, object, rhs);
        }
    }

    for (const auto *element : properties) {
        RegScope propScope(pg);

        const ir::Property *propExpr = element->AsProperty();
        const ir::Expression *key = propExpr->Key();
        const auto [init, target] = GetAssignmentTarget(propExpr);

        Operand propOperand = pg->ToOwnPropertyKey(key, propExpr->IsComputed());

        auto lref = JSLReference::Create(pg, target, object->IsDeclaration());

        if (std::holds_alternative<VReg>(propOperand)) {
            pg->LoadAccumulator(element, std::get<VReg>(propOperand));
            pg->LoadObjByValue(element, rhs);
        } else {
            pg->LoadAccumulator(element, rhs);
            pg->LoadObjProperty(element, propOperand);
        }

        GenDefaultInitializer(pg, element, init);

        lref.SetValue();
    }
}

void Destructuring::Compile(PandaGen *pg, const ir::Expression *pattern)
{
    RegScope rs(pg);

    VReg rhs = pg->AllocReg();
    pg->StoreAccumulator(pattern, rhs);

    if (pattern->IsArrayPattern()) {
        GenArray(pg, pattern->AsArrayPattern());
    } else {
        GenObject(pg, pattern->AsObjectPattern(), rhs);
    }

    pg->LoadAccumulator(pattern, rhs);
}
}  // namespace panda::es2panda::compiler