/**
 * 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.
 */

#ifndef ASSEMBLER_ASSEMBLY_INS_H
#define ASSEMBLER_ASSEMBLY_INS_H

#include <array>
#include <string>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector>

#include "assembly-debug.h"
#include "bytecode_emitter.h"
#include "file_items.h"
#include "isa.h"
#include "lexer.h"

namespace panda::pandasm {

enum class Opcode {
#define OPLIST(opcode, name, optype, width, flags, def_idx, use_idxs) opcode,
    PANDA_INSTRUCTION_LIST(OPLIST)
#undef OPLIST
        INVALID,
    NUM_OPCODES = INVALID
};

enum InstFlags {
    NONE = 0,
    JUMP = (1U << 0U),
    COND = (1U << 1U),
    CALL = (1U << 2U),
    RETURN = (1U << 3U),
    ACC_READ = (1U << 4U),
    ACC_WRITE = (1U << 5U),
    PSEUDO = (1U << 6U),
    THROWING = (1U << 7U),
    METHOD_ID = (1U << 8U),
    FIELD_ID = (1U << 9U),
    TYPE_ID = (1U << 10U),
    STRING_ID = (1U << 11U),
    LITERALARRAY_ID = (1U << 12U),
    CALL_RANGE = (1U << 13U)
};

constexpr int INVALID_REG_IDX = -1;

constexpr size_t MAX_NUMBER_OF_SRC_REGS = 4;  // TODO(mbolshov): auto-generate

constexpr InstFlags operator|(InstFlags a, InstFlags b)
{
    using utype = std::underlying_type_t<InstFlags>;
    return static_cast<InstFlags>(static_cast<utype>(a) | static_cast<utype>(b));
}

#define OPLIST(opcode, name, optype, width, flags, def_idx, use_idxs) flags,
constexpr std::array<unsigned, static_cast<size_t>(Opcode::NUM_OPCODES)> INST_FLAGS_TABLE = {
    PANDA_INSTRUCTION_LIST(OPLIST)};
#undef OPLIST

#define OPLIST(opcode, name, optype, width, flags, def_idx, use_idxs) width,
constexpr std::array<size_t, static_cast<size_t>(Opcode::NUM_OPCODES)> INST_WIDTH_TABLE = {
    PANDA_INSTRUCTION_LIST(OPLIST)};
#undef OPLIST

#define OPLIST(opcode, name, optype, width, flags, def_idx, use_idxs) def_idx,
constexpr std::array<int, static_cast<size_t>(Opcode::NUM_OPCODES)> DEF_IDX_TABLE = {PANDA_INSTRUCTION_LIST(OPLIST)};
#undef OPLIST

#define OPLIST(opcode, name, optype, width, flags, def_idx, use_idxs) use_idxs,
// clang-format off
constexpr std::array<std::array<int, MAX_NUMBER_OF_SRC_REGS>, static_cast<size_t>(Opcode::NUM_OPCODES)> USE_IDXS_TABLE = {
    PANDA_INSTRUCTION_LIST(OPLIST)};
// clang-format on
#undef OPLIST

struct Ins {
    using IType = std::variant<int64_t, double>;

    constexpr static uint16_t ACCUMULATOR = -1;
    constexpr static size_t MAX_CALL_SHORT_ARGS = 2;
    constexpr static size_t MAX_CALL_ARGS = 4;
    constexpr static uint16_t MAX_NON_RANGE_CALL_REG = 15;
    constexpr static uint16_t MAX_RANGE_CALL_START_REG = 255;

    Opcode opcode = Opcode::INVALID; /* operation type */
    std::vector<uint16_t> regs;      /* list of arguments - registers */
    std::vector<std::string> ids;    /* list of arguments - identifiers */
    std::vector<IType> imms;         /* list of arguments - immediates */
    std::string label;               /* label at the beginning of a line */
    bool set_label = false;          /* whether this label is defined */
    debuginfo::Ins ins_debug;

    std::string ToString(std::string endline = "", bool print_args = false, size_t first_arg_idx = 0) const;

    bool Emit(BytecodeEmitter &emitter, panda_file::MethodItem *method,
              const std::unordered_map<std::string, panda_file::BaseMethodItem *> &methods,
              const std::unordered_map<std::string, panda_file::BaseFieldItem *> &fields,
              const std::unordered_map<std::string, panda_file::BaseClassItem *> &classes,
              const std::unordered_map<std::string, panda_file::StringItem *> &strings,
              const std::unordered_map<std::string, panda_file::LiteralArrayItem *> &literalarrays,
              const std::unordered_map<std::string_view, panda::Label> &labels) const;

    size_t OperandListLength() const
    {
        return regs.size() + ids.size() + imms.size();
    }

    bool HasFlag(InstFlags flag) const
    {
        if (opcode == Opcode::INVALID) {  // TODO(mbolshov): introduce 'label' opcode for labels
            return false;
        }
        return (INST_FLAGS_TABLE[static_cast<size_t>(opcode)] & flag) != 0;
    }

    bool CanThrow() const
    {
        return HasFlag(InstFlags::THROWING) || HasFlag(InstFlags::METHOD_ID) || HasFlag(InstFlags::FIELD_ID) ||
               HasFlag(InstFlags::TYPE_ID) || HasFlag(InstFlags::STRING_ID);
    }

    bool IsJump() const
    {
        return HasFlag(InstFlags::JUMP);
    }

    bool IsConditionalJump() const
    {
        return IsJump() && HasFlag(InstFlags::COND);
    }

    bool IsCall() const
    {  // Non-range call
        return HasFlag(InstFlags::CALL);
    }

    bool IsCallRange() const
    {  // Range call
        return HasFlag(InstFlags::CALL_RANGE);
    }

    bool IsPseudoCall() const
    {
        return HasFlag(InstFlags::PSEUDO) && HasFlag(InstFlags::CALL);
    }

    bool IsReturn() const
    {
        return HasFlag(InstFlags::RETURN);
    }

    size_t MaxRegEncodingWidth() const
    {
        if (opcode == Opcode::INVALID) {
            return 0;
        }
        return INST_WIDTH_TABLE[static_cast<size_t>(opcode)];
    }

    std::vector<uint16_t> Uses() const
    {
        if (IsPseudoCall()) {
            return regs;
        }

        if (opcode == Opcode::INVALID) {
            return {};
        }

        auto use_idxs = USE_IDXS_TABLE[static_cast<size_t>(opcode)];
        std::vector<uint16_t> res(MAX_NUMBER_OF_SRC_REGS + 1);
        if (HasFlag(InstFlags::ACC_READ)) {
            res.push_back(Ins::ACCUMULATOR);
        }
        for (auto idx : use_idxs) {
            if (idx == INVALID_REG_IDX) {
                break;
            }
            ASSERT(static_cast<size_t>(idx) < regs.size());
            res.emplace_back(regs[idx]);
        }
        return res;
    }

    std::optional<size_t> Def() const
    {
        if (opcode == Opcode::INVALID) {
            return {};
        }
        auto def_idx = DEF_IDX_TABLE[static_cast<size_t>(opcode)];
        if (def_idx != INVALID_REG_IDX) {
            return regs[def_idx];
        }
        if (HasFlag(InstFlags::ACC_WRITE)) {
            return Ins::ACCUMULATOR;
        }
        return {};
    }

    bool IsValidToEmit() const
    {
        const auto INVALID_REG_NUM = 1U << MaxRegEncodingWidth();
        for (auto reg : regs) {
            if (reg >= INVALID_REG_NUM) {
                return false;
            }
        }
        return true;
    }

    bool HasDebugInfo() const
    {
        return ins_debug.line_number != 0;
    }

private:
    std::string OperandsToString(bool print_args = false, size_t first_arg_idx = 0) const;
    std::string RegsToString(bool &first, bool print_args = false, size_t first_arg_idx = 0) const;
    std::string ImmsToString(bool &first) const;
    std::string IdsToString(bool &first) const;

    std::string IdToString(size_t idx, bool is_first) const;
    std::string ImmToString(size_t idx, bool is_first) const;
    std::string RegToString(size_t idx, bool is_first, bool print_args = false, size_t first_arg_idx = 0) const;
};
}  // namespace panda::pandasm

#endif  // ASSEMBLER_ASSEMBLY_INS_H