#include "app.h"
#include "checkerboard.h"
#include "isogfx-demo.h"

#include <isogfx/isogfx.h>

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

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

static const int SCREEN_WIDTH  = 1408;
static const int SCREEN_HEIGHT = 960;

typedef struct State {
  Gfx*      gfx;
  IsoGfx*   iso;
  IsoGfxApp app;
  Texture*  screen_texture;
  Scene*    scene;
} State;

static bool init(const GfxAppDesc* desc, void** app_state) {
  State* state = calloc(1, sizeof(State));
  if (!state) {
    return false;
  }

  if (!(state->iso = isogfx_new(&(IsoGfxDesc){
            .screen_width = SCREEN_WIDTH, .screen_height = SCREEN_HEIGHT}))) {
    goto cleanup;
  }
  //  if (!make_checkerboard_app(state->iso, &state->app)) {
  //    goto cleanup;
  //  }
  if (!make_demo_app(state->iso, &state->app)) {
    goto cleanup;
  }

  // Apply pixel scaling if requested by the app.
  int texture_width, texture_height;
  if (state->app.pixel_scale > 1) {
    texture_width  = SCREEN_WIDTH / state->app.pixel_scale;
    texture_height = SCREEN_HEIGHT / state->app.pixel_scale;
    isogfx_resize(state->iso, texture_width, texture_height);
  } else {
    texture_width  = SCREEN_WIDTH;
    texture_height = SCREEN_HEIGHT;
  }

  if (!(state->gfx = gfx_init())) {
    goto cleanup;
  }
  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);

  if (!(state->screen_texture = gfx_make_texture(
            render_backend, &(TextureDesc){
                                .width     = texture_width,
                                .height    = texture_height,
                                .dimension = Texture2D,
                                .format    = TextureSRGBA8,
                                .filtering = NearestFiltering,
                                .wrap      = ClampToEdge,
                                .mipmaps   = false}))) {
    goto cleanup;
  }

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

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

  MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1};
  material_desc.uniforms[0]  = (ShaderUniform){
       .type          = UniformTexture,
       .value.texture = state->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();
  if (!object) {
    goto cleanup;
  }
  gfx_add_object_mesh(object, mesh);

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

  *app_state = state;
  return true;

cleanup:
  if (state->gfx) {
    gfx_destroy(&state->gfx);
  }
  free(state);
  return false;
}

static void shutdown(void* app_state) {
  assert(app_state);
  State* state = (State*)(app_state);

  if (state->app.state) {
    assert(state->iso);
    (*state->app.shutdown)(state->iso, state->app.state);
  }
  isogfx_del(&state->iso);
  gfx_destroy(&state->gfx);
  free(app_state);
}

static void update(void* app_state, double t, double dt) {
  assert(app_state);
  State* state = (State*)(app_state);

  isogfx_update(state->iso, t);

  assert(state->app.update);
  (*state->app.update)(state->iso, state->app.state, t, dt);
}

static void render(void* app_state) {
  assert(app_state);
  State* state = (State*)(app_state);

  assert(state->app.render);
  (*state->app.render)(state->iso, state->app.state);

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

  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
  Renderer*      renderer       = gfx_get_renderer(state->gfx);

  gfx_start_frame(render_backend);
  gfx_render_scene(
      renderer, &(RenderSceneParams){
                    .mode = RenderDefault, .scene = state->scene, .camera = 0});
  gfx_end_frame(render_backend);
}

static void resize(void* app_state, int width, int height) {
  assert(app_state);
  State* state = (State*)(app_state);

  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
  gfx_set_viewport(render_backend, width, height);
}

int main(int argc, const char** argv) {
  const int initial_width  = SCREEN_WIDTH;
  const int initial_height = SCREEN_HEIGHT;
  const int max_fps        = 60;

  gfx_app_run(
      &(GfxAppDesc){
          .argc              = argc,
          .argv              = argv,
          .width             = initial_width,
          .height            = initial_height,
          .max_fps           = max_fps,
          .update_delta_time = max_fps > 0 ? 1.0 / (double)max_fps : 0.0,
          .title             = "Isometric Renderer"},
      &(GfxAppCallbacks){
          .init     = init,
          .update   = update,
          .render   = render,
          .resize   = resize,
          .shutdown = shutdown});

  return 0;
}