#include <isogfx/backend.h>
#include <isogfx/isogfx.h>

#include <gfx/core.h>
#include <gfx/gfx.h>
#include <gfx/renderer.h>
#include <gfx/scene.h>
#include <gfx/util/geometry.h>
#include <gfx/util/shader.h>

#include <assert.h>
#include <stdlib.h>

typedef struct IsoBackend {
  Gfx*   gfx;
  Scene* scene;
  /// The screen or "iso screen" refers to the colour buffer of the iso graphics
  /// library. This texture is used to draw the iso screen onto the graphics
  /// window.
  Texture* screen_texture;
  /// Window size.
  int window_width;
  int window_height;
  /// The viewport refers to the area inside the window to which screen_texture
  /// is drawn. It is a scaled version of the iso screen, scaled while
  /// respecting the iso screen's aspect ratio to prevent distortion.
  int    viewport_x, viewport_y, viewport_width, viewport_height;
  double stretch; // Stretch factor from iso screen dimensions to viewport
                  // dimensions.
} IsoBackend;

IsoBackend* IsoBackendInit(const IsoGfx* iso) {
  assert(iso);

  IsoBackend* backend = calloc(1, sizeof(IsoBackend));
  if (!backend) {
    return 0;
  }

  if (!(backend->gfx = gfx_init())) {
    goto cleanup;
  }
  GfxCore* gfxcore = gfx_get_core(backend->gfx);

  int screen_width, screen_height;
  isogfx_get_screen_size(iso, &screen_width, &screen_height);

  if (!(backend->screen_texture = gfx_make_texture(
            gfxcore, &(TextureDesc){
                         .width     = screen_width,
                         .height    = screen_height,
                         .dimension = Texture2D,
                         .format    = TextureSRGBA8,
                         .filtering = NearestFiltering,
                         .wrap      = ClampToEdge,
                         .mipmaps   = false}))) {
    goto cleanup;
  }

  ShaderProgram* shader = gfx_make_view_texture_shader(gfxcore);
  if (!shader) {
    goto cleanup;
  }

  Geometry* geometry = gfx_make_quad_11(gfxcore);
  if (!geometry) {
    goto cleanup;
  }

  MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1};
  material_desc.uniforms[0]  = (ShaderUniform){
       .type          = UniformTexture,
       .value.texture = backend->screen_texture,
       .name          = sstring_make("Texture")};
  Material* material = gfx_make_material(&material_desc);
  if (!material) {
    return false;
  }

  const MeshDesc mesh_desc =
      (MeshDesc){.geometry = geometry, .material = material, .shader = shader};
  Mesh* mesh = gfx_make_mesh(&mesh_desc);
  if (!mesh) {
    goto cleanup;
  }

  SceneObject* object =
      gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}});
  if (!object) {
    goto cleanup;
  }

  backend->scene  = gfx_make_scene();
  SceneNode* node = gfx_make_object_node(object);
  SceneNode* root = gfx_get_scene_root(backend->scene);
  gfx_set_node_parent(node, root);

  return backend;

cleanup:
  if (backend->gfx) {
    gfx_destroy(&backend->gfx);
  }
  free(backend);
  return 0;
}

void IsoBackendShutdown(IsoBackend** ppApp) {
  assert(ppApp);

  IsoBackend* app = *ppApp;
  if (!app) {
    return;
  }

  gfx_destroy(&app->gfx);
}

void IsoBackendResizeWindow(
    IsoBackend* app, const IsoGfx* iso, int width, int height) {
  assert(app);
  assert(iso);

  app->window_width  = width;
  app->window_height = height;

  // Virtual screen dimensions.
  int screen_width, screen_height;
  isogfx_get_screen_size(iso, &screen_width, &screen_height);

  // Stretch the virtual screen onto the viewport while respecting the screen's
  // aspect ratio to prevent distortion.
  if (width > height) { // Wide screen.
    app->stretch         = (double)height / (double)screen_height;
    app->viewport_width  = (int)((double)screen_width * app->stretch);
    app->viewport_height = height;
    app->viewport_x      = (width - app->viewport_width) / 2;
    app->viewport_y      = 0;
  } else { // Tall screen.
    app->stretch         = (double)width / (double)screen_width;
    app->viewport_width  = width;
    app->viewport_height = (int)((float)screen_height * app->stretch);
    app->viewport_x      = 0;
    app->viewport_y      = (height - app->viewport_height) / 2;
  }
}

void IsoBackendRender(const IsoBackend* app, const IsoGfx* iso) {
  assert(app);
  assert(iso);

  const Pixel* screen = isogfx_get_screen_buffer(iso);
  assert(screen);
  gfx_update_texture(app->screen_texture, &(TextureDataDesc){.pixels = screen});

  GfxCore*  gfxcore  = gfx_get_core(app->gfx);
  Renderer* renderer = gfx_get_renderer(app->gfx);

  // Clear the whole window.
  gfx_set_viewport(gfxcore, 0, 0, app->window_width, app->window_height);
  gfx_clear(gfxcore, vec4_make(0, 0, 0, 0));

  // Draw to the subregion where the virtual screen can stretch without
  // distortion.
  gfx_set_viewport(
      gfxcore, app->viewport_x, app->viewport_y, app->viewport_width,
      app->viewport_height);

  // Render the iso screen.
  gfx_start_frame(gfxcore);
  gfx_render_scene(
      renderer, &(RenderSceneParams){
                    .mode = RenderDefault, .scene = app->scene, .camera = 0});
  gfx_end_frame(gfxcore);
}

bool IsoBackendGetMousePosition(
    const IsoBackend* app, double window_x, double window_y, double* x,
    double* y) {
  assert(app);

  // Translate from window coordinates to the subregion where the stretched
  // iso screen is rendered.
  const double screen_x = window_x - app->viewport_x;
  const double screen_y = window_y - app->viewport_y;

  // Position may be out of bounds.
  if ((0 <= screen_x) && (screen_x < app->viewport_width) && (0 <= screen_y) &&
      (screen_y < app->viewport_height)) {
    // Scale back from the stretched subregion to the iso screen dimensions.
    *x = screen_x / app->stretch;
    *y = screen_y / app->stretch;
    return true;
  } else {
    *x = -1;
    *y = -1;
    return false;
  }
}