#include <isogfx/app.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;
static const int MAX_FPS       = 60;

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

typedef struct GfxAppState {
  AppState state;
} GfxAppState;

static bool init(GfxAppState* gfx_app_state, int argc, const char** argv) {
  assert(gfx_app_state);
  AppState* state = &gfx_app_state->state;

  IsoGfxApp* app = state->app;

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

  if (!(*app->init)(app->state, state->iso, argc, argv)) {
    goto cleanup;
  }

  // Apply pixel scaling if requested by the app.
  int texture_width, texture_height;
  if (app->pixel_scale > 1) {
    texture_width  = SCREEN_WIDTH / app->pixel_scale;
    texture_height = SCREEN_HEIGHT / 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(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}});
  if (!object) {
    goto cleanup;
  }

  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);

  return true;

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

static void shutdown(GfxAppState* gfx_app_state) {
  assert(gfx_app_state);
  AppState* state = &gfx_app_state->state;

  if (state->app) {
    assert(state->iso);
    (*state->app->shutdown)(state->app->state, state->iso);
  }

  isogfx_del(&state->iso);
  gfx_destroy(&state->gfx);
}

static void update(GfxAppState* gfx_app_state, double t, double dt) {
  assert(gfx_app_state);
  AppState* state = &gfx_app_state->state;

  isogfx_update(state->iso, t);

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

static void render(GfxAppState* gfx_app_state) {
  assert(gfx_app_state);
  AppState* state = &gfx_app_state->state;

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

  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(GfxAppState* gfx_app_state, int width, int height) {
  assert(gfx_app_state);
  AppState* state = &gfx_app_state->state;

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

void iso_run(int argc, const char** argv, IsoGfxApp* app) {
  GfxAppState app_state = {
      .state = (AppState){
                          .app = app,
                          }
  };
  gfx_app_run(
      &(GfxAppDesc){
          .argc              = argc,
          .argv              = argv,
          .width             = SCREEN_WIDTH,
          .height            = SCREEN_HEIGHT,
          .max_fps           = MAX_FPS,
          .update_delta_time = MAX_FPS > 0 ? 1.0 / (double)MAX_FPS : 0.0,
          .title             = "Isometric Renderer",
          .app_state         = &app_state},
      &(GfxAppCallbacks){
          .init     = init,
          .update   = update,
          .render   = render,
          .resize   = resize,
          .shutdown = shutdown});
}