// Copyright 2018 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <array>
#include <cstddef>
#include <optional>
#include <string>

#include "Common/BitField.h"
#include "Common/CommonTypes.h"

namespace Core
{
class CPUThreadGuard;
class System;
};  // namespace Core
namespace Memory
{
class MemoryManager;
};

namespace PowerPC
{
class PowerPCManager;
struct PowerPCState;

enum class RequestedAddressSpace
{
  Effective,  // whatever the current MMU state is
  Physical,   // as if the MMU was turned off
  Virtual,    // specifically want MMU turned on, fails if off
};

template <typename T>
struct ReadResult
{
  // whether the address had to be translated (given address was treated as virtual) or not (given
  // address was treated as physical)
  bool translated;

  // the actual value that was read
  T value;

  ReadResult(bool translated_, T&& value_) : translated(translated_), value(std::forward<T>(value_))
  {
  }
  ReadResult(bool translated_, const T& value_) : translated(translated_), value(value_) {}
};

struct WriteResult
{
  // whether the address had to be translated (given address was treated as virtual) or not (given
  // address was treated as physical)
  bool translated;

  explicit WriteResult(bool translated_) : translated(translated_) {}
};

constexpr int BAT_INDEX_SHIFT = 17;
constexpr u32 BAT_PAGE_SIZE = 1 << BAT_INDEX_SHIFT;
constexpr u32 BAT_PAGE_COUNT = 1 << (32 - BAT_INDEX_SHIFT);
constexpr u32 BAT_MAPPED_BIT = 0x1;
constexpr u32 BAT_PHYSICAL_BIT = 0x2;
constexpr u32 BAT_WI_BIT = 0x4;
constexpr u32 BAT_RESULT_MASK = UINT32_C(~0x7);
using BatTable = std::array<u32, BAT_PAGE_COUNT>;  // 128 KB

constexpr size_t HW_PAGE_SIZE = 4096;
constexpr size_t HW_PAGE_MASK = HW_PAGE_SIZE - 1;
constexpr u32 HW_PAGE_INDEX_SHIFT = 12;
constexpr u32 HW_PAGE_INDEX_MASK = 0x3f;

// Return value of MMU::TryReadInstruction().
struct TryReadInstResult
{
  bool valid;
  bool from_bat;
  u32 hex;
  u32 physical_address;
};

// Return value of MMU::JitCache_TranslateAddress().
struct TranslateResult
{
  bool valid = false;
  bool translated = false;
  bool from_bat = false;
  u32 address = 0;

  TranslateResult() = default;
  explicit TranslateResult(u32 address_) : valid(true), address(address_) {}
  TranslateResult(bool from_bat_, u32 address_)
      : valid(true), translated(true), from_bat(from_bat_), address(address_)
  {
  }
};

enum class XCheckTLBFlag
{
  NoException,
  Read,
  Write,
  Opcode,
  OpcodeNoException
};

class MMU
{
public:
  MMU(Core::System& system, Memory::MemoryManager& memory, PowerPC::PowerPCManager& power_pc);
  MMU(const MMU& other) = delete;
  MMU(MMU&& other) = delete;
  MMU& operator=(const MMU& other) = delete;
  MMU& operator=(MMU&& other) = delete;
  ~MMU();

  // Routines for debugger UI, cheats, etc. to access emulated memory from the
  // perspective of the CPU.  Not for use by core emulation routines.
  // Use "Host" prefix.

  // Reads a value from emulated memory using the currently active MMU settings.
  // If the read fails (eg. address does not correspond to a mapped address in the current address
  // space), a PanicAlert will be shown to the user and zero (or an empty string for the string
  // case) will be returned.
  static u8 HostRead_U8(const Core::CPUThreadGuard& guard, u32 address);
  static u16 HostRead_U16(const Core::CPUThreadGuard& guard, u32 address);
  static u32 HostRead_U32(const Core::CPUThreadGuard& guard, u32 address);
  static u64 HostRead_U64(const Core::CPUThreadGuard& guard, u32 address);
  static float HostRead_F32(const Core::CPUThreadGuard& guard, u32 address);
  static double HostRead_F64(const Core::CPUThreadGuard& guard, u32 address);
  static u32 HostRead_Instruction(const Core::CPUThreadGuard& guard, u32 address);
  static std::string HostGetString(const Core::CPUThreadGuard& guard, u32 address, size_t size = 0);

  // Try to read a value from emulated memory at the given address in the given memory space.
  // If the read succeeds, the returned value will be present and the ReadResult contains the read
  // value and information on whether the given address had to be translated or not. Unlike the
  // HostRead functions, this does not raise a user-visible alert on failure.
  static std::optional<ReadResult<u8>>
  HostTryReadU8(const Core::CPUThreadGuard& guard, u32 address,
                RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<ReadResult<u16>>
  HostTryReadU16(const Core::CPUThreadGuard& guard, u32 address,
                 RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<ReadResult<u32>>
  HostTryReadU32(const Core::CPUThreadGuard& guard, u32 address,
                 RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<ReadResult<u64>>
  HostTryReadU64(const Core::CPUThreadGuard& guard, u32 address,
                 RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<ReadResult<float>>
  HostTryReadF32(const Core::CPUThreadGuard& guard, u32 address,
                 RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<ReadResult<double>>
  HostTryReadF64(const Core::CPUThreadGuard& guard, u32 address,
                 RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<ReadResult<u32>>
  HostTryReadInstruction(const Core::CPUThreadGuard& guard, u32 address,
                         RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<ReadResult<std::string>>
  HostTryReadString(const Core::CPUThreadGuard& guard, u32 address, size_t size = 0,
                    RequestedAddressSpace space = RequestedAddressSpace::Effective);

  // Writes a value to emulated memory using the currently active MMU settings.
  // If the write fails (eg. address does not correspond to a mapped address in the current address
  // space), a PanicAlert will be shown to the user.
  static void HostWrite_U8(const Core::CPUThreadGuard& guard, u32 var, u32 address);
  static void HostWrite_U16(const Core::CPUThreadGuard& guard, u32 var, u32 address);
  static void HostWrite_U32(const Core::CPUThreadGuard& guard, u32 var, u32 address);
  static void HostWrite_U64(const Core::CPUThreadGuard& guard, u64 var, u32 address);
  static void HostWrite_F32(const Core::CPUThreadGuard& guard, float var, u32 address);
  static void HostWrite_F64(const Core::CPUThreadGuard& guard, double var, u32 address);

  // Try to a write a value to memory at the given address in the given memory space.
  // If the write succeeds, the returned TryWriteResult contains information on whether the given
  // address had to be translated or not. Unlike the HostWrite functions, this does not raise a
  // user-visible alert on failure.
  static std::optional<WriteResult>
  HostTryWriteU8(const Core::CPUThreadGuard& guard, u32 var, const u32 address,
                 RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<WriteResult>
  HostTryWriteU16(const Core::CPUThreadGuard& guard, u32 var, const u32 address,
                  RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<WriteResult>
  HostTryWriteU32(const Core::CPUThreadGuard& guard, u32 var, const u32 address,
                  RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<WriteResult>
  HostTryWriteU64(const Core::CPUThreadGuard& guard, u64 var, const u32 address,
                  RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<WriteResult>
  HostTryWriteF32(const Core::CPUThreadGuard& guard, float var, const u32 address,
                  RequestedAddressSpace space = RequestedAddressSpace::Effective);
  static std::optional<WriteResult>
  HostTryWriteF64(const Core::CPUThreadGuard& guard, double var, const u32 address,
                  RequestedAddressSpace space = RequestedAddressSpace::Effective);

  // Returns whether a read or write to the given address will resolve to a RAM access in the given
  // address space.
  static bool HostIsRAMAddress(const Core::CPUThreadGuard& guard, u32 address,
                               RequestedAddressSpace space = RequestedAddressSpace::Effective);

  // Same as HostIsRAMAddress, but uses IBAT instead of DBAT.
  static bool
  HostIsInstructionRAMAddress(const Core::CPUThreadGuard& guard, u32 address,
                              RequestedAddressSpace space = RequestedAddressSpace::Effective);

  // Routines for the CPU core to access memory.

  // Used by interpreter to read instructions, uses iCache
  u32 Read_Opcode(u32 address);
  TryReadInstResult TryReadInstruction(u32 address);

  u8 Read_U8(u32 address);
  u16 Read_U16(u32 address);
  u32 Read_U32(u32 address);
  u64 Read_U64(u32 address);

  void Write_U8(u32 var, u32 address);
  void Write_U16(u32 var, u32 address);
  void Write_U32(u32 var, u32 address);
  void Write_U64(u64 var, u32 address);

  void Write_U16_Swap(u32 var, u32 address);
  void Write_U32_Swap(u32 var, u32 address);
  void Write_U64_Swap(u64 var, u32 address);

  void DMA_LCToMemory(u32 mem_address, u32 cache_address, u32 num_blocks);
  void DMA_MemoryToLC(u32 cache_address, u32 mem_address, u32 num_blocks);

  void ClearDCacheLine(u32 address);  // Zeroes 32 bytes; address should be 32-byte-aligned
  void StoreDCacheLine(u32 address);
  void InvalidateDCacheLine(u32 address);
  void FlushDCacheLine(u32 address);
  void TouchDCacheLine(u32 address, bool store);

  // TLB functions
  void SDRUpdated();
  void InvalidateTLBEntry(u32 address);
  void DBATUpdated();
  void IBATUpdated();

  // Result changes based on the BAT registers and MSR.DR.  Returns whether
  // it's safe to optimize a read or write to this address to an unguarded
  // memory access.  Does not consider page tables.
  bool IsOptimizableRAMAddress(u32 address) const;
  u32 IsOptimizableMMIOAccess(u32 address, u32 access_size) const;
  bool IsOptimizableGatherPipeWrite(u32 address) const;

  TranslateResult JitCache_TranslateAddress(u32 address);

  std::optional<u32> GetTranslatedAddress(u32 address);

  BatTable& GetIBATTable() { return m_ibat_table; }
  BatTable& GetDBATTable() { return m_dbat_table; }

private:
  enum class TranslateAddressResultEnum : u8
  {
    BAT_TRANSLATED,
    PAGE_TABLE_TRANSLATED,
    DIRECT_STORE_SEGMENT,
    PAGE_FAULT,
  };

  struct TranslateAddressResult
  {
    u32 address;
    TranslateAddressResultEnum result;
    bool wi;  // Set to true if the view of memory is either write-through or cache-inhibited

    TranslateAddressResult(TranslateAddressResultEnum result_, u32 address_, bool wi_ = false)
        : address(address_), result(result_), wi(wi_)
    {
    }
    bool Success() const { return result <= TranslateAddressResultEnum::PAGE_TABLE_TRANSLATED; }
  };

  union EffectiveAddress
  {
    BitField<0, 12, u32> offset;
    BitField<12, 16, u32> page_index;
    BitField<22, 6, u32> API;
    BitField<28, 4, u32> SR;

    u32 Hex = 0;

    EffectiveAddress() = default;
    explicit EffectiveAddress(u32 address) : Hex{address} {}
  };

  template <const XCheckTLBFlag flag>
  TranslateAddressResult TranslateAddress(u32 address);

  TranslateAddressResult TranslatePageAddress(const EffectiveAddress address,
                                              const XCheckTLBFlag flag, bool* wi);

  void GenerateDSIException(u32 effective_address, bool write);
  void GenerateISIException(u32 effective_address);

  void Memcheck(u32 address, u64 var, bool write, size_t size);

  void UpdateBATs(BatTable& bat_table, u32 base_spr);
  void UpdateFakeMMUBat(BatTable& bat_table, u32 start_addr);

  template <XCheckTLBFlag flag, typename T, bool never_translate = false>
  T ReadFromHardware(u32 em_address);
  template <XCheckTLBFlag flag, bool never_translate = false>
  void WriteToHardware(u32 em_address, const u32 data, const u32 size);
  template <XCheckTLBFlag flag>
  bool IsRAMAddress(u32 address, bool translate);

  template <typename T>
  static std::optional<ReadResult<T>> HostTryReadUX(const Core::CPUThreadGuard& guard,
                                                    const u32 address, RequestedAddressSpace space);
  static std::optional<WriteResult> HostTryWriteUX(const Core::CPUThreadGuard& guard, const u32 var,
                                                   const u32 address, const u32 size,
                                                   RequestedAddressSpace space);

  Core::System& m_system;
  Memory::MemoryManager& m_memory;
  PowerPC::PowerPCManager& m_power_pc;
  PowerPC::PowerPCState& m_ppc_state;

  BatTable m_ibat_table;
  BatTable m_dbat_table;
};

void ClearDCacheLineFromJit64(MMU& mmu, u32 address);
u32 ReadU8ZXFromJit64(MMU& mmu, u32 address);   // Returns zero-extended 32bit value
u32 ReadU16ZXFromJit64(MMU& mmu, u32 address);  // Returns zero-extended 32bit value
u32 ReadU32FromJit64(MMU& mmu, u32 address);
u64 ReadU64FromJit64(MMU& mmu, u32 address);
void WriteU8FromJit64(MMU& mmu, u32 var, u32 address);
void WriteU16FromJit64(MMU& mmu, u32 var, u32 address);
void WriteU32FromJit64(MMU& mmu, u32 var, u32 address);
void WriteU64FromJit64(MMU& mmu, u64 var, u32 address);
void WriteU16SwapFromJit64(MMU& mmu, u32 var, u32 address);
void WriteU32SwapFromJit64(MMU& mmu, u32 var, u32 address);
void WriteU64SwapFromJit64(MMU& mmu, u64 var, u32 address);

// The JitArm64 function that calls these has very specific register allocation that's difficult to
// change, so we have a separate set of functions here for it. This can probably be refactored in
// the future.
void ClearDCacheLineFromJitArm64(u32 address, MMU& mmu);
u8 ReadU8FromJitArm64(u32 address, MMU& mmu);
u16 ReadU16FromJitArm64(u32 address, MMU& mmu);
u32 ReadU32FromJitArm64(u32 address, MMU& mmu);
u64 ReadU64FromJitArm64(u32 address, MMU& mmu);
void WriteU8FromJitArm64(u32 var, u32 address, MMU& mmu);
void WriteU16FromJitArm64(u32 var, u32 address, MMU& mmu);
void WriteU32FromJitArm64(u32 var, u32 address, MMU& mmu);
void WriteU64FromJitArm64(u64 var, u32 address, MMU& mmu);
void WriteU16SwapFromJitArm64(u32 var, u32 address, MMU& mmu);
void WriteU32SwapFromJitArm64(u32 var, u32 address, MMU& mmu);
void WriteU64SwapFromJitArm64(u64 var, u32 address, MMU& mmu);
}  // namespace PowerPC
