#include <stdint.h>

#include "memorymap.hpp"

#include "holly/background.hpp"
#include "holly/core.hpp"
#include "holly/isp_tsp.hpp"
#include "holly/object_list_data.hpp"
#include "holly/region_array.hpp"
#include "holly/texture_memory_alloc9.hpp"

#include "math/float_types.hpp"

#include "reference/reference_render.data.h"

const struct opb_size opb_size = { .opaque = 0
                                 , .opaque_modifier = 8 * 4
                                 , .translucent = 0
                                 , .translucent_modifier = 0
                                 , .punch_through = 8 * 4
                                 };

static const int framebuffer_width = 32;
static const int framebuffer_height = 32;
static const int tile_width = framebuffer_width / 32;
static const int tile_height = framebuffer_height / 32;

struct triangle_parameter_vertex {
  float x;
  float y;
  float z;
  uint32_t color1;
  uint32_t color2;
};

struct triangle_parameter {
  uint32_t isp_tsp_instruction_word;
  uint32_t tsp_instruction_word_0;
  uint32_t texture_control_word_0;
  uint32_t tsp_instruction_word_1;
  uint32_t texture_control_word_1;
  triangle_parameter_vertex a;
  triangle_parameter_vertex b;
  triangle_parameter_vertex c;
};

struct modifier_volume_parameter_vertex {
  float x;
  float y;
  float z;
};

struct modifier_volume_parameter {
  uint32_t isp_tsp_instruction_word;
  uint32_t pad1;
  uint32_t pad2;
  modifier_volume_parameter_vertex a;
  modifier_volume_parameter_vertex b;
  modifier_volume_parameter_vertex c;
};

template <int N>
struct object_pointer_block {
  uint32_t pointer[N];
};
static_assert((sizeof (object_pointer_block<8>)) == 32);

using vec2i = vec<2, int>;

static const int opaque_modifier_start = 0;
static const int punch_through_start = (sizeof (triangle_parameter));

static volatile uint8_t * const object_list = (volatile uint8_t *)(&texture_memory32[texture_memory_alloc.object_list.start / 4]);
static volatile uint8_t * const isp_tsp_parameters = (volatile uint8_t *)(&texture_memory32[texture_memory_alloc.isp_tsp_parameters.start / 4]);

static inline void transfer_object_list(volatile uint8_t * mem)
{
  auto blocks = reinterpret_cast<volatile object_pointer_block<8> *>(mem);

  { // opaque modifier
    auto& block = blocks[0];
    block.pointer[0] = object_list_data::pointer_type::triangle_array
                     | object_list_data::triangle_array::number_of_triangles(0)
                     | object_list_data::triangle_array::skip(0)
                     | object_list_data::triangle_array::start(opaque_modifier_start / 4);

    block.pointer[1] = object_list_data::pointer_type::object_pointer_block_link
                     | object_list_data::object_pointer_block_link::end_of_list;
  }

  { // punch through
    auto& block = blocks[1];
    block.pointer[0] = object_list_data::pointer_type::triangle_array
                     | object_list_data::triangle_array::number_of_triangles(0)
                     | object_list_data::triangle_array::shadow
                     | object_list_data::triangle_array::skip(1)
                     | object_list_data::triangle_array::start(punch_through_start / 4);

    block.pointer[1] = object_list_data::pointer_type::object_pointer_block_link
                     | object_list_data::object_pointer_block_link::end_of_list;
  }
}

static inline void transfer_isp_tsp_parameters(volatile uint8_t * mem,
                                               const vec3& ap, const vec2i& ac,
                                               const vec3& bp, const vec2i& bc,
                                               const vec3& cp, const vec2i& cc)
{
  auto params = reinterpret_cast<volatile triangle_parameter *>(mem);
  params->isp_tsp_instruction_word = isp_tsp_instruction_word::depth_compare_mode::greater
                                   | isp_tsp_instruction_word::culling_mode::no_culling
                                   | isp_tsp_instruction_word::gouraud_shading;

  params->tsp_instruction_word_0 = tsp_instruction_word::src_alpha_instr::one
                                 | tsp_instruction_word::dst_alpha_instr::zero
                                 | tsp_instruction_word::fog_control::no_fog;

  params->texture_control_word_0 = 0;

  params->tsp_instruction_word_1 = tsp_instruction_word::src_alpha_instr::one
                                 | tsp_instruction_word::dst_alpha_instr::one
                                 | tsp_instruction_word::fog_control::no_fog;

  params->texture_control_word_1 = 0;

  params->a.x = ap.x;
  params->a.y = ap.y;
  params->a.z = ap.z;
  params->a.color1 = ac.x;
  params->a.color2 = ac.y;

  params->b.x = bp.x;
  params->b.y = bp.y;
  params->b.z = bp.z;
  params->b.color1 = bc.x;
  params->b.color2 = bc.y;

  params->c.x = cp.x;
  params->c.y = cp.y;
  params->c.z = cp.z;
  params->c.color1 = cc.x;
  params->c.color2 = cc.y;
}

static inline void transfer_modifier_volume_isp_tsp_parameters(volatile uint8_t * mem,
                                                               const vec3& ap,
                                                               const vec3& bp,
                                                               const vec3& cp)
{
  auto params = reinterpret_cast<volatile modifier_volume_parameter *>(mem);
  params->isp_tsp_instruction_word = isp_tsp_instruction_word::volume_instruction::inside_last_polygon
                                   | isp_tsp_instruction_word::culling_mode::no_culling;

  params->pad1 = 0;
  params->pad2 = 0;

  params->a.x = ap.x;
  params->a.y = ap.y;
  params->a.z = ap.z;

  params->b.x = bp.x;
  params->b.y = bp.y;
  params->b.z = bp.z;

  params->c.x = cp.x;
  params->c.y = cp.y;
  params->c.z = cp.z;
}

static void transfer_punch_through()
{
  vec3 ap = {18.2192f,     1.0f, 0.01f};
  vec3 bp = {27.8808f, 25.4219f, 0.01f};
  vec3 cp = {    1.9f, 21.5781f, 0.01f};

  vec2i ac = {0x0000ff, 0xff0000};
  vec2i bc = {0x00ff00, 0x0000ff};
  vec2i cc = {0xff0000, 0x00ff00};

  transfer_isp_tsp_parameters(&isp_tsp_parameters[punch_through_start],
                              ap, ac,
                              bp, bc,
                              cp, cc);
}

static void transfer_opaque_modifier()
{
  vec3 ap = {  0.0f, -50.0f, 0.1f};
  vec3 bp = {100.0f,   0.0f, 0.1f};
  vec3 cp = {  0.0f,  50.0f, 0.1f};

  transfer_modifier_volume_isp_tsp_parameters(&isp_tsp_parameters[opaque_modifier_start],
                                              ap,
                                              bp,
                                              cp);
}

static void init_texture_memory()
{
  region_array_multipass(tile_width,
			 tile_height,
			 &opb_size,
			 1,
			 texture_memory_alloc.region_array.start,
			 texture_memory_alloc.object_list.start);

  background_parameter2(texture_memory_alloc.background[0].start,
                        0xff000000);

  transfer_object_list(object_list);

  transfer_punch_through();

  transfer_opaque_modifier();
}

template <typename T>
static inline bool compare_equal(T a, T b, int length)
{
  for (int i = 0; i < length; i++) {
    if (a[i] != b[i])
      return false;
  }
  return true;
}

bool detect_emulator()
{
  init_texture_memory();

  bool dither = true;
  core_start_render2(texture_memory_alloc.region_array.start,
                     texture_memory_alloc.isp_tsp_parameters.start,
                     texture_memory_alloc.background[0].start,
                     texture_memory_alloc.framebuffer[0].start,
                     framebuffer_width,
                     dither);
  core_wait_end_of_render_video();

  uint32_t * a = (uint32_t *)&_binary_reference_reference_render_data_start;
  uint32_t * b = (uint32_t *)&texture_memory32[texture_memory_alloc.framebuffer[0].start / 4];
  int length = 32 * 32 * 2 / 4;

  int equal = compare_equal(a, b, length);

  for (uint32_t i = 0; i < (8 * 1024 * 1024 / 32); i++) {
    asm volatile ("ocbp @%0"
                  :                          // output
                  : "r" (((uint32_t)texture_memory32) + (32 * i)) // input
                  : "memory");
  }

  return !equal;
}
