/*!
 * @file Instruction.cpp
 * An EE instruction, represented as an operation, plus a list of source/destination atoms.
 * Can print itself (within the context of a LinkedObjectFile).
 */

#include "Instruction.h"

#include "common/util/Assert.h"

#include "decompiler/ObjectFile/LinkedObjectFile.h"

#include "third-party/fmt/core.h"

namespace decompiler {
/*!
 * Convert atom to a string for disassembly.
 */
std::string InstructionAtom::to_string(const std::vector<DecompilerLabel>& labels) const {
  switch (kind) {
    case REGISTER:
      return reg.to_string();
    case IMM:
      return std::to_string(imm);
    case LABEL:
      return labels.at(label_id).name;
    case VU_ACC:
      return "acc";
    case VU_Q:
      return "Q";
    case IMM_SYM:
      return sym;
    case VF_FIELD:
      ASSERT(imm >= 0 && imm < 4);
      return fmt::format(".{}", "xyzw"[imm]);
    default:
      throw std::runtime_error("Unsupported InstructionAtom");
  }
}

/*!
 * Make this atom a register.
 */
void InstructionAtom::set_reg(Register r) {
  kind = REGISTER;
  reg = r;
}

/*!
 * Make this atom an immediate.
 */
void InstructionAtom::set_imm(int32_t i) {
  kind = IMM;
  imm = i;
}

/*!
 * Make this atom a label.
 */
void InstructionAtom::set_label(int id) {
  kind = LABEL;
  label_id = id;
}

/*!
 * Make this atom the VU ACC register.
 */
void InstructionAtom::set_vu_acc() {
  kind = VU_ACC;
}

/*!
 * Make this atom the VU0 Q register.
 */
void InstructionAtom::set_vu_q() {
  kind = VU_Q;
}

/*!
 * Make this atom a symbol.
 */
void InstructionAtom::set_sym(std::string _sym) {
  kind = IMM_SYM;
  sym = std::move(_sym);
}

/*!
 * Make this atom a field (x,y,z,w) of a vf.
 */
void InstructionAtom::set_vf_field(uint32_t value) {
  kind = VF_FIELD;
  imm = value;
  ASSERT(value < 4);
}

/*!
 * Get as register, or error if not a register.
 */
Register InstructionAtom::get_reg() const {
  ASSERT(kind == REGISTER);
  return reg;
}

/*!
 * Get as integer immediate, or error if not an integer immediate.
 */
int32_t InstructionAtom::get_imm() const {
  ASSERT(kind == IMM);
  return imm;
}

/*!
 * Get the VF_FIELD as an integer immediate, or error if not applicable.
 */
int32_t InstructionAtom::get_vf_field() const {
  ASSERT(kind == VF_FIELD);
  return imm;
}

/*!
 * Get as label index, or error if not a label.
 */
int InstructionAtom::get_label() const {
  ASSERT(kind == LABEL);
  return label_id;
}

/*!
 * Get as symbol, or error if not a symbol.
 */
std::string InstructionAtom::get_sym() const {
  ASSERT(kind == IMM_SYM);
  return sym;
}

/*!
 * True if this atom is some sort of constant that doesn't involve linking.
 */
bool InstructionAtom::is_link_or_label() const {
  return kind == IMM_SYM || kind == LABEL;
}

bool InstructionAtom::operator==(const InstructionAtom& other) const {
  if (kind != other.kind) {
    return false;
  }
  switch (kind) {
    case REGISTER:
      return reg == other.reg;
    case IMM:
      return imm == other.imm;
    case LABEL:
      return label_id == other.label_id;
    case VU_ACC:
    case VU_Q:
      return true;
    default:
      ASSERT(false);
      return false;
  }
}

char Instruction::cop2_bc_to_char() const {
  switch (cop2_bc) {
    case 0:
      return 'x';
    case 1:
      return 'y';
    case 2:
      return 'z';
    case 3:
      return 'w';
    default:
      return '?';
  }
}
std::string Instruction::cop2_dest_to_char() const {
  std::string dest = ".";
  if (cop2_dest & 8)
    dest.push_back('x');
  if (cop2_dest & 4)
    dest.push_back('y');
  if (cop2_dest & 2)
    dest.push_back('z');
  if (cop2_dest & 1)
    dest.push_back('w');
  return dest;
}

int Instruction::cop2_dest_mask_intel() const {
  int mask = 0;
  for (int i = 0; i < 4; i++) {        // x,y,z,w order
    if (cop2_dest & (1 << (3 - i))) {  // set for ps2
      mask |= (1 << i);
    }
  }
  return mask;
}

/*!
 * Convert just the name of the opcode to a string, omitting src/dst, but including
 * suffixes (interlock, broadcasts and destination)
 */
std::string Instruction::op_name_to_string() const {
  auto& info = gOpcodeInfo[(int)kind];

  // the name
  std::string result = info.name;

  // optional "interlock" specification.
  if (il != 0xff) {
    result.append(il ? ".i" : ".ni");
  }

  // optional "broadcast" specification for COP2 opcodes.
  if (cop2_bc != 0xff) {
    result.push_back(cop2_bc_to_char());
  }

  // optional "destination" specification for COP2 opcodes.
  if (cop2_dest != 0xff) {
    result.append(cop2_dest_to_char());
  }

  return result;
}

/*!
 * Convert entire instruction to a string.
 */
std::string Instruction::to_string(const std::vector<DecompilerLabel>& labels) const {
  auto& info = gOpcodeInfo[(int)kind];
  auto result = op_name_to_string();

  // relative store and load instructions have a special syntax in MIPS
  if (info.is_store) {
    ASSERT(n_dst == 0);
    ASSERT(n_src == 3);
    result += " ";
    result += src[0].to_string(labels);
    result += ", ";
    result += src[1].to_string(labels);
    result += "(";
    result += src[2].to_string(labels);
    result += ")";
  } else if (info.is_load) {
    ASSERT(n_dst == 1);
    ASSERT(n_src == 2);
    result += " ";
    result += dst[0].to_string(labels);
    result += ", ";
    result += src[0].to_string(labels);
    result += "(";
    result += src[1].to_string(labels);
    result += ")";
  } else {
    // for instructions that aren't a store or load, the dest/sources are comma separated.
    bool end_comma = false;

    for (uint8_t i = 0; i < n_dst; i++) {
      result += " " + dst[i].to_string(labels) + ",";
      end_comma = true;
    }

    for (uint8_t i = 0; i < n_src; i++) {
      if (src[i].kind == InstructionAtom::VF_FIELD) {
        if (end_comma) {
          result.pop_back();
        }
        result += src[i].to_string(labels) + ",";
      } else {
        result += " " + src[i].to_string(labels) + ",";
      }
      end_comma = true;
    }

    if (end_comma) {
      result.pop_back();
    }
  }

  return result;
}

/*!
 * Was this instruction successfully decoded?
 */
bool Instruction::is_valid() const {
  return kind != InstructionKind::UNKNOWN;
}

/*!
 * Add a destination atom to this Instruction
 */
void Instruction::add_dst(InstructionAtom& a) {
  ASSERT(n_dst < MAX_INTRUCTION_DEST);
  dst[n_dst++] = a;
}

/*!
 * Add a source atom to this Instruction
 */
void Instruction::add_src(InstructionAtom& a) {
  ASSERT(n_src < MAX_INSTRUCTION_SOURCE);
  src[n_src++] = a;
}

/*!
 * Get a source atom that's an immediate, or error if it doesn't exist.
 */
InstructionAtom& Instruction::get_imm_src() {
  for (int i = 0; i < n_src; i++) {
    if (src[i].kind == InstructionAtom::IMM) {
      return src[i];
    }
  }
  ASSERT(false);
  return src[0];
}

/*!
 * Try to find a src which is an integer immediate, and return it as an integer.
 */
int32_t Instruction::get_imm_src_int() {
  return get_imm_src().get_imm();
}

/*!
 * Safe get dst atom
 */
InstructionAtom& Instruction::get_dst(size_t idx) {
  ASSERT(idx < n_dst);
  return dst[idx];
}

/*!
 * Safe get src atom
 */
InstructionAtom& Instruction::get_src(size_t idx) {
  ASSERT(idx < n_src);
  return src[idx];
}

/*!
 * Safe get dst atom
 */
const InstructionAtom& Instruction::get_dst(size_t idx) const {
  ASSERT(idx < n_dst);
  return dst[idx];
}

/*!
 * Safe get src atom
 */
const InstructionAtom& Instruction::get_src(size_t idx) const {
  ASSERT(idx < n_src);
  return src[idx];
}

/*!
 * Get OpcodeInfo for the opcode used in this instruction.
 */
const OpcodeInfo& Instruction::get_info() const {
  return gOpcodeInfo[int(kind)];
}

/*!
 * Get the target label for this instruction. If the instruction doesn't have a target label,
 * return -1.
 */
int Instruction::get_label_target() const {
  int result = -1;
  for (int i = 0; i < n_src; i++) {
    if (src[i].kind == InstructionAtom::AtomKind::LABEL) {
      ASSERT(result == -1);
      result = src[i].get_label();
    }
  }
  return result;
}

bool Instruction::operator==(const Instruction& other) const {
  if (kind != other.kind || n_src != other.n_src || n_dst != other.n_dst ||
      cop2_dest != other.cop2_dest || cop2_bc != other.cop2_bc || il != other.il) {
    return false;
  }

  for (int i = 0; i < n_dst; i++) {
    if (dst[i] != other.dst[i]) {
      return false;
    }
  }

  for (int i = 0; i < n_src; i++) {
    if (src[i] != other.src[i]) {
      return false;
    }
  }

  return true;
}
}  // namespace decompiler
