#include <bit>

#include "systembus.hpp"
#include "systembus_bits.hpp"

#include "holly/holly.hpp"
#include "holly/ta_bits.hpp"
#include "holly/region_array.hpp"
#include "holly/background.hpp"
#include "holly/core_bits.hpp"
#include "holly/core.hpp"
#include "holly/framebuffer.hpp"
#include "holly/texture_memory_alloc9.hpp"
#include "holly/ta_fifo_texture_memory_transfer.hpp"
#include "holly/ta_fifo_polygon_converter.hpp"
#include "holly/video_output.hpp"
#include "dve.hpp"

#include "math/float_types.hpp"
#include "math/transform.hpp"

#include "platform/ta_parameter_presets.hpp"
#include "platform/graphics_primitive.hpp"
#include "platform/graphics.hpp"
#include "platform/input.hpp"
#include "platform/font.hpp"
#include "platform/emulator_detected.hpp"

#include "demo/ballistics.hpp"
#include "demo/bridge.hpp"
#include "demo/sailboat.hpp"
#include "demo/collision.hpp"
#include "demo/lizard/main.hpp"

#include "maple/maple_bus_bits.hpp"

namespace graphics {

  static uint8_t ta_buf[1024 * 1024] __attribute__((aligned(32)));
  static ta_parameter_writer writer(ta_buf, (sizeof (ta_buf)));

  static volatile int ta_in_use = 0;
  static volatile int core_in_use = 0;
  static volatile int next_frame = 0;
  static volatile int framebuffer_ix = 0;
  static volatile int next_frame_ix = 0;

  constexpr uint32_t ta_alloc
      = ta_alloc_ctrl::pt_opb::_32x4byte
      | ta_alloc_ctrl::tm_opb::no_list
      | ta_alloc_ctrl::t_opb::_32x4byte
      | ta_alloc_ctrl::om_opb::_32x4byte
      | ta_alloc_ctrl::o_opb::_32x4byte;

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

  mat4x4 view_trans;

  void interrupt(uint32_t istnrm)
  {
    if (istnrm & istnrm::v_blank_in) {
      system.ISTNRM = istnrm::v_blank_in;

      next_frame = 1;
      holly.FB_R_SOF1 = texture_memory_alloc.framebuffer[next_frame_ix].start;
      holly.FB_R_SOF2 = texture_memory_alloc.framebuffer[next_frame_ix].start;
    }

    if (istnrm & istnrm::end_of_render_tsp) {
      system.ISTNRM = istnrm::end_of_render_tsp
        | istnrm::end_of_render_isp
        | istnrm::end_of_render_video;

      next_frame_ix = framebuffer_ix;
      framebuffer_ix += 1;
      if (framebuffer_ix >= 3) framebuffer_ix = 0;

      core_in_use = 0;
    }

    if (istnrm & istnrm::end_of_transferring_opaque_list) {
      system.ISTNRM = istnrm::end_of_transferring_opaque_list;

      core_in_use = 1;
      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[framebuffer_ix].start,
                         framebuffer::framebuffer.px_width);

      ta_in_use = 0;
    }
  }

  //demo::ballistics _ballistics;
  //demo::bridge _bridge;
  //demo::sailboat _sailboat;
  //demo::collision _collision;
  demo::lizard _lizard;

  demo::scene * scenes[] = {
    &_lizard,
    //&_bridge,
    //&_ballistics
  };

  int current_scene_ix = 0;

  demo::scene * current_scene = scenes[current_scene_ix];

  void draw()
  {
    holly.PT_ALPHA_REF = 15;

    current_scene->draw(writer, view_trans * scale(0.05f));
  }

  void init()
  {
    core_init();
    framebuffer::scaler_init();
    view_trans = current_scene->init();
    // read
    //while (spg_status::vsync(holly.SPG_STATUS));
    //while (!spg_status::vsync(holly.SPG_STATUS));

    system.LMMODE0 = 1; // 32-bit address space
    system.LMMODE1 = 1; // 32-bit address space

    int length = texture_memory_alloc.framebuffer[2].start;
    ta_fifo_texture_memory_transfer::zeroize(&ta_fifo_texture_memory[0 / 4],
                                             length,
                                             0x00c0bebc);
    ta_fifo_texture_memory_transfer::zeroize(&ta_fifo_texture_memory[length / 4],
                                             720 * 480 * 2,
                                             0xc5f7c5f7);

    //framebuffer::spg_set_mode_640x480_vga();
    //framebuffer::spg_set_mode_720x480_vga();
    //framebuffer::init(720, 480,
    uint32_t cable_type = video_output::get_cable_type();
    if (emulator_detected) {
      framebuffer::spg_set_mode_640x480_vga();
      framebuffer::init(640, 480,
                        texture_memory_alloc.framebuffer[0].start);
    } else {
      switch (cable_type) {
      case pdtra::cable_type::vga:
        framebuffer::spg_set_mode_640x480_vga();
        framebuffer::init(640, 480,
                          texture_memory_alloc.framebuffer[0].start);
        break;
      default:
        framebuffer::spg_set_mode_320x240_ntsc_ni();
        framebuffer::init(320, 240,
                          texture_memory_alloc.framebuffer[0].start);
        break;
      }
    }
    core_param_init(texture_memory_alloc.region_array.start,
                    texture_memory_alloc.isp_tsp_parameters.start,
                    texture_memory_alloc.background[0].start,
                    framebuffer::framebuffer.px_width);

    holly.SOFTRESET = softreset::pipeline_soft_reset
                    | softreset::ta_soft_reset;
    holly.SOFTRESET = 0;

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

    region_array_multipass(framebuffer::framebuffer.tile_width(),
                           framebuffer::framebuffer.tile_height(),
                           &opb_size,
                           1,
                           texture_memory_alloc.region_array.start,
                           texture_memory_alloc.object_list.start);
  }

  void input_events()
  {
    struct button_state {
      uint8_t a;
      uint8_t b;
      uint8_t x;
      uint8_t y;
      uint8_t start;
    };
    static button_state last[4] = {};

    for (int port_ix = 0; port_ix < 4; port_ix++) {
      auto& port = input::state.port[port_ix];
      if ((port.function_type & function_type::controller) != 0 && port.host_response_data_transfer_ft0 != nullptr) {
        auto& bus_data = port.host_response_data_transfer_ft0->bus_data;
        auto& data_fields = bus_data.data_fields;
        if ((std::byteswap(data_fields.function_type) & function_type::controller) == 0)
          continue;

        auto& data = data_fields.data;

        float dl = static_cast<float>(data.analog_coordinate_axis[0]) * (1.f / 255.f);
        float dr = static_cast<float>(data.analog_coordinate_axis[1]) * (1.f / 255.f);
        float dx = static_cast<float>(data.analog_coordinate_axis[2] - 0x80) * (1.f / 128.f);
        float dy = static_cast<float>(data.analog_coordinate_axis[3] - 0x80) * (1.f / 128.f);

        bool a = ft0::data_transfer::digital_button::a(data.digital_button) == 0;
        bool b = ft0::data_transfer::digital_button::b(data.digital_button) == 0;
        bool x = ft0::data_transfer::digital_button::x(data.digital_button) == 0;
        bool y = ft0::data_transfer::digital_button::y(data.digital_button) == 0;

        bool ra = ft0::data_transfer::digital_button::ra(data.digital_button) == 0;
        bool la = ft0::data_transfer::digital_button::la(data.digital_button) == 0;
        bool da = ft0::data_transfer::digital_button::da(data.digital_button) == 0;
        bool ua = ft0::data_transfer::digital_button::ua(data.digital_button) == 0;

        bool start = ft0::data_transfer::digital_button::start(data.digital_button) == 0;

        if (last[port_ix].a != a && a) current_scene->a();
        if (b) current_scene->b();
        if (last[port_ix].x != x && x) current_scene->x();
        if (last[port_ix].y != y && y) current_scene->y();

        if (ra) current_scene->ra();
        if (la) current_scene->la();
        if (da) current_scene->da();
        if (ua) current_scene->ua();

        //if (start) current_scene->start();

        current_scene->analog(dl, dr, dx, dy);

        //view_trans = rotate_y(dy) * view_trans;// * rotate_x(dx);

        if (last[port_ix].start != start && start && x) {
          //current_scene_ix += 1;
          //current_scene = scenes[current_scene_ix % 3];
          //view_trans = current_scene->init(emulator);
        }

        last[port_ix].a = a;
        last[port_ix].b = b;
        last[port_ix].x = x;
        last[port_ix].y = y;
        last[port_ix].start = start;
      }
    }
  }

  void step()
  {
    input_events();
    current_scene->update();

    writer.offset = 0;
    draw();

    while (ta_in_use);
    while (core_in_use);
    ta_in_use = 1;
    ta_polygon_converter_init2(texture_memory_alloc.isp_tsp_parameters.start,
                               texture_memory_alloc.isp_tsp_parameters.end,
                               texture_memory_alloc.object_list.start,
                               texture_memory_alloc.object_list.end,
                               opb_size.total(),
                               ta_alloc,
                               framebuffer::framebuffer.tile_width(),
                               framebuffer::framebuffer.tile_height());

    ta_polygon_converter_writeback(writer.buf, writer.offset);
    ta_polygon_converter_transfer(writer.buf, writer.offset);

    while (next_frame == 0);
    next_frame = 0;
  }
}
