// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <algorithm>

#include "Common/Arm64Emitter.h"
#include "Common/BitSet.h"
#include "Common/CommonTypes.h"

#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/HW/GPFifo.h"
#include "Core/PowerPC/JitArm64/Jit.h"
#include "Core/PowerPC/JitArm64/JitArm64_RegCache.h"
#include "Core/PowerPC/PPCTables.h"
#include "Core/PowerPC/PowerPC.h"

using namespace Arm64Gen;

void JitArm64::lfXX(UGeckoInstruction inst)
{
  INSTRUCTION_START
  JITDISABLE(bJITLoadStoreFloatingOff);
  FALLBACK_IF(jo.memcheck);

  u32 a = inst.RA, b = inst.RB;

  s32 offset = inst.SIMM_16;
  u32 flags = BackPatchInfo::FLAG_LOAD;
  bool update = false;
  s32 offset_reg = -1;

  switch (inst.OPCD)
  {
  case 31:
    switch (inst.SUBOP10)
    {
    case 567:  // lfsux
      flags |= BackPatchInfo::FLAG_SIZE_F32;
      update = true;
      offset_reg = b;
      break;
    case 535:  // lfsx
      flags |= BackPatchInfo::FLAG_SIZE_F32;
      offset_reg = b;
      break;
    case 631:  // lfdux
      flags |= BackPatchInfo::FLAG_SIZE_F64;
      update = true;
      offset_reg = b;
      break;
    case 599:  // lfdx
      flags |= BackPatchInfo::FLAG_SIZE_F64;
      offset_reg = b;
      break;
    }
    break;
  case 49:  // lfsu
    flags |= BackPatchInfo::FLAG_SIZE_F32;
    update = true;
    break;
  case 48:  // lfs
    flags |= BackPatchInfo::FLAG_SIZE_F32;
    break;
  case 51:  // lfdu
    flags |= BackPatchInfo::FLAG_SIZE_F64;
    update = true;
    break;
  case 50:  // lfd
    flags |= BackPatchInfo::FLAG_SIZE_F64;
    break;
  }

  u32 imm_addr = 0;
  bool is_immediate = false;

  RegType type = !!(flags & BackPatchInfo::FLAG_SIZE_F64) ? REG_LOWER_PAIR : REG_DUP_SINGLE;

  gpr.Lock(W0, W30);
  fpr.Lock(Q0);

  ARM64Reg VD = fpr.RW(inst.FD, type);
  ARM64Reg addr_reg = W0;

  if (update)
  {
    // Always uses RA
    if (gpr.IsImm(a) && offset_reg == -1)
    {
      is_immediate = true;
      imm_addr = offset + gpr.GetImm(a);
    }
    else if (gpr.IsImm(a) && offset_reg != -1 && gpr.IsImm(offset_reg))
    {
      is_immediate = true;
      imm_addr = gpr.GetImm(a) + gpr.GetImm(offset_reg);
    }
    else
    {
      if (offset_reg == -1)
      {
        if (offset >= 0 && offset < 4096)
        {
          ADD(addr_reg, gpr.R(a), offset);
        }
        else if (offset < 0 && offset > -4096)
        {
          SUB(addr_reg, gpr.R(a), std::abs(offset));
        }
        else
        {
          MOVI2R(addr_reg, offset);
          ADD(addr_reg, addr_reg, gpr.R(a));
        }
      }
      else
      {
        ADD(addr_reg, gpr.R(offset_reg), gpr.R(a));
      }
    }
  }
  else
  {
    if (offset_reg == -1)
    {
      if (a && gpr.IsImm(a))
      {
        is_immediate = true;
        imm_addr = gpr.GetImm(a) + offset;
      }
      else if (a)
      {
        if (offset >= 0 && offset < 4096)
        {
          ADD(addr_reg, gpr.R(a), offset);
        }
        else if (offset < 0 && offset > -4096)
        {
          SUB(addr_reg, gpr.R(a), std::abs(offset));
        }
        else
        {
          MOVI2R(addr_reg, offset);
          ADD(addr_reg, addr_reg, gpr.R(a));
        }
      }
      else
      {
        is_immediate = true;
        imm_addr = offset;
      }
    }
    else
    {
      if (a && gpr.IsImm(a) && gpr.IsImm(offset_reg))
      {
        is_immediate = true;
        imm_addr = gpr.GetImm(a) + gpr.GetImm(offset_reg);
      }
      else if (!a && gpr.IsImm(offset_reg))
      {
        is_immediate = true;
        imm_addr = gpr.GetImm(offset_reg);
      }
      else if (a)
      {
        ADD(addr_reg, gpr.R(a), gpr.R(offset_reg));
      }
      else
      {
        MOV(addr_reg, gpr.R(offset_reg));
      }
    }
  }

  ARM64Reg XA = EncodeRegTo64(addr_reg);

  if (is_immediate)
    MOVI2R(XA, imm_addr);

  if (update)
  {
    gpr.BindToRegister(a, false);
    MOV(gpr.R(a), addr_reg);
  }

  BitSet32 regs_in_use = gpr.GetCallerSavedUsed();
  BitSet32 fprs_in_use = fpr.GetCallerSavedUsed();
  regs_in_use[W0] = 0;
  fprs_in_use[0] = 0;  // Q0
  fprs_in_use[VD - Q0] = 0;

  if (is_immediate && PowerPC::IsOptimizableRAMAddress(imm_addr))
  {
    EmitBackpatchRoutine(flags, true, false, VD, XA, BitSet32(0), BitSet32(0));
  }
  else
  {
    EmitBackpatchRoutine(flags, jo.fastmem, jo.fastmem, VD, XA, regs_in_use, fprs_in_use);
  }

  gpr.Unlock(W0, W30);
  fpr.Unlock(Q0);
}

void JitArm64::stfXX(UGeckoInstruction inst)
{
  INSTRUCTION_START
  JITDISABLE(bJITLoadStoreFloatingOff);
  FALLBACK_IF(jo.memcheck);

  u32 a = inst.RA, b = inst.RB;

  s32 offset = inst.SIMM_16;
  u32 flags = BackPatchInfo::FLAG_STORE;
  bool update = false;
  s32 offset_reg = -1;

  switch (inst.OPCD)
  {
  case 31:
    switch (inst.SUBOP10)
    {
    case 663:  // stfsx
      flags |= BackPatchInfo::FLAG_SIZE_F32;
      offset_reg = b;
      break;
    case 695:  // stfsux
      flags |= BackPatchInfo::FLAG_SIZE_F32;
      update = true;
      offset_reg = b;
      break;
    case 727:  // stfdx
      flags |= BackPatchInfo::FLAG_SIZE_F64;
      offset_reg = b;
      break;
    case 759:  // stfdux
      flags |= BackPatchInfo::FLAG_SIZE_F64;
      update = true;
      offset_reg = b;
      break;
    case 983:  // stfiwx
      flags |= BackPatchInfo::FLAG_SIZE_F32I;
      offset_reg = b;
      break;
    }
    break;
  case 53:  // stfsu
    flags |= BackPatchInfo::FLAG_SIZE_F32;
    update = true;
    break;
  case 52:  // stfs
    flags |= BackPatchInfo::FLAG_SIZE_F32;
    break;
  case 55:  // stfdu
    flags |= BackPatchInfo::FLAG_SIZE_F64;
    update = true;
    break;
  case 54:  // stfd
    flags |= BackPatchInfo::FLAG_SIZE_F64;
    break;
  }

  u32 imm_addr = 0;
  bool is_immediate = false;

  gpr.Lock(W0, W1, W30);
  fpr.Lock(Q0);

  bool single = (flags & BackPatchInfo::FLAG_SIZE_F32) && fpr.IsSingle(inst.FS, true);

  ARM64Reg V0 = fpr.R(inst.FS, single ? REG_LOWER_PAIR_SINGLE : REG_LOWER_PAIR);

  if (single)
  {
    flags &= ~BackPatchInfo::FLAG_SIZE_F32;
    flags |= BackPatchInfo::FLAG_SIZE_F32I;
  }

  ARM64Reg addr_reg = W1;

  if (update)
  {
    // Always uses RA
    if (gpr.IsImm(a) && offset_reg == -1)
    {
      is_immediate = true;
      imm_addr = offset + gpr.GetImm(a);
    }
    else if (gpr.IsImm(a) && offset_reg != -1 && gpr.IsImm(offset_reg))
    {
      is_immediate = true;
      imm_addr = gpr.GetImm(a) + gpr.GetImm(offset_reg);
    }
    else
    {
      if (offset_reg == -1)
      {
        if (offset >= 0 && offset < 4096)
        {
          ADD(addr_reg, gpr.R(a), offset);
        }
        else if (offset < 0 && offset > -4096)
        {
          SUB(addr_reg, gpr.R(a), std::abs(offset));
        }
        else
        {
          MOVI2R(addr_reg, offset);
          ADD(addr_reg, addr_reg, gpr.R(a));
        }
      }
      else
      {
        ADD(addr_reg, gpr.R(offset_reg), gpr.R(a));
      }
    }
  }
  else
  {
    if (offset_reg == -1)
    {
      if (a && gpr.IsImm(a))
      {
        is_immediate = true;
        imm_addr = gpr.GetImm(a) + offset;
      }
      else if (a)
      {
        if (offset >= 0 && offset < 4096)
        {
          ADD(addr_reg, gpr.R(a), offset);
        }
        else if (offset < 0 && offset > -4096)
        {
          SUB(addr_reg, gpr.R(a), std::abs(offset));
        }
        else
        {
          MOVI2R(addr_reg, offset);
          ADD(addr_reg, addr_reg, gpr.R(a));
        }
      }
      else
      {
        is_immediate = true;
        imm_addr = offset;
      }
    }
    else
    {
      if (a && gpr.IsImm(a) && gpr.IsImm(offset_reg))
      {
        is_immediate = true;
        imm_addr = gpr.GetImm(a) + gpr.GetImm(offset_reg);
      }
      else if (!a && gpr.IsImm(offset_reg))
      {
        is_immediate = true;
        imm_addr = gpr.GetImm(offset_reg);
      }
      else if (a)
      {
        ADD(addr_reg, gpr.R(a), gpr.R(offset_reg));
      }
      else
      {
        MOV(addr_reg, gpr.R(offset_reg));
      }
    }
  }

  ARM64Reg XA = EncodeRegTo64(addr_reg);

  if (is_immediate &&
      !(jit->jo.optimizeGatherPipe && PowerPC::IsOptimizableGatherPipeWrite(imm_addr)))
  {
    MOVI2R(XA, imm_addr);

    if (update)
    {
      gpr.BindToRegister(a, false);
      MOV(gpr.R(a), addr_reg);
    }
  }
  else if (!is_immediate && update)
  {
    gpr.BindToRegister(a, false);
    MOV(gpr.R(a), addr_reg);
  }

  BitSet32 regs_in_use = gpr.GetCallerSavedUsed();
  BitSet32 fprs_in_use = fpr.GetCallerSavedUsed();
  regs_in_use[W0] = 0;
  regs_in_use[W1] = 0;
  fprs_in_use[0] = 0;  // Q0

  if (is_immediate)
  {
    if (jit->jo.optimizeGatherPipe && PowerPC::IsOptimizableGatherPipeWrite(imm_addr))
    {
      int accessSize;
      if (flags & BackPatchInfo::FLAG_SIZE_F64)
        accessSize = 64;
      else
        accessSize = 32;

      u64 base_ptr = std::min((u64)&GPFifo::m_gatherPipeCount, (u64)&GPFifo::m_gatherPipe);
      u32 count_off = (u64)&GPFifo::m_gatherPipeCount - base_ptr;
      u32 pipe_off = (u64)&GPFifo::m_gatherPipe - base_ptr;

      MOVI2R(X30, base_ptr);

      if (pipe_off)
        ADD(X1, X30, pipe_off);

      LDR(INDEX_UNSIGNED, W0, X30, count_off);
      if (flags & BackPatchInfo::FLAG_SIZE_F64)
      {
        m_float_emit.REV64(8, Q0, V0);
      }
      else if (flags & BackPatchInfo::FLAG_SIZE_F32)
      {
        m_float_emit.FCVT(32, 64, D0, EncodeRegToDouble(V0));
        m_float_emit.REV32(8, D0, D0);
      }
      else if (flags & BackPatchInfo::FLAG_SIZE_F32I)
      {
        m_float_emit.REV32(8, D0, V0);
      }

      if (pipe_off)
      {
        m_float_emit.STR(accessSize, accessSize == 64 ? Q0 : D0, X1, ArithOption(X0));
      }
      else
      {
        m_float_emit.STR(accessSize, accessSize == 64 ? Q0 : D0, X30, ArithOption(X0));
      }

      ADD(W0, W0, accessSize >> 3);
      STR(INDEX_UNSIGNED, W0, X30, count_off);
      js.fifoBytesThisBlock += accessSize >> 3;

      if (update)
      {
        // Chance of this happening is fairly low, but support it
        gpr.BindToRegister(a, false);
        MOVI2R(gpr.R(a), imm_addr);
      }
    }
    else if (PowerPC::IsOptimizableRAMAddress(imm_addr))
    {
      EmitBackpatchRoutine(flags, true, false, V0, XA, BitSet32(0), BitSet32(0));
    }
    else
    {
      EmitBackpatchRoutine(flags, false, false, V0, XA, regs_in_use, fprs_in_use);
    }
  }
  else
  {
    EmitBackpatchRoutine(flags, jo.fastmem, jo.fastmem, V0, XA, regs_in_use, fprs_in_use);
  }
  gpr.Unlock(W0, W1, W30);
  fpr.Unlock(Q0);
}
