#include <isogfx/isogfx.h>

#include <filesystem.h>
#include <mem.h>
#include <mempool.h>
#include <path.h>

#include <linux/limits.h>

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/// Maximum number of tiles unless the user specifies a value.
#define DEFAULT_MAX_NUM_TILES 1024

/// Maximum number of sprites unless the user specifies a value.
#define DEFAULT_MAX_NUM_SPRITES 128

/// Size of sprite sheet pool in bytes unless the user specifies a value.
#define DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES (8 * 1024 * 1024)

/// Default animation speed.
#define ANIMATION_FPS 10

/// Time between animation updates.
#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS)

typedef struct ivec2 {
  int x, y;
} ivec2;

typedef struct vec2 {
  double x, y;
} vec2;

// -----------------------------------------------------------------------------
// Tile set (TS) and tile map (TM) file formats.
// -----------------------------------------------------------------------------

/// Maximum length of path strings in .TS and .TM files.
#define MAX_PATH_LENGTH 128

typedef struct Ts_Tile {
  uint16_t width;     /// Tile width in pixels.
  uint16_t height;    /// Tile height in pixels.
  Pixel    pixels[1]; /// Count: width * height.
} Ts_Tile;

typedef struct Ts_TileSet {
  uint16_t num_tiles;
  uint16_t max_tile_width;  /// Maximum tile width in pixels.
  uint16_t max_tile_height; /// Maximum tile height in pixels.
  Ts_Tile  tiles[1];        /// Count: num_tiles.
} Ts_TileSet;

typedef struct Tm_Layer {
  union {
    char tileset_path[MAX_PATH_LENGTH]; // Relative to the Tm_Map file.
  };
  Tile tiles[1]; /// Count: world_width * world_height.
} Tm_Layer;

typedef struct Tm_Map {
  uint16_t world_width;  /// World width in number of tiles.
  uint16_t world_height; /// World height in number of tiles.
  uint16_t base_tile_width;
  uint16_t base_tile_height;
  uint16_t num_layers;
  Tm_Layer layers[1]; // Count: num_layers.
} Tm_Map;

static inline const Tm_Layer* tm_map_get_next_layer(
    const Tm_Map* map, const Tm_Layer* layer) {
  assert(map);
  assert(layer);
  return (const Tm_Layer*)((const uint8_t*)layer + sizeof(Tm_Layer) +
                           ((map->world_width * map->world_height - 1) *
                            sizeof(Tile)));
}

static inline const Ts_Tile* ts_tileset_get_next_tile(
    const Ts_TileSet* tileset, const Ts_Tile* tile) {
  assert(tileset);
  assert(tile);
  return (const Ts_Tile*)((const uint8_t*)tile + sizeof(Ts_Tile) +
                          ((tile->width * tile->height - 1) * sizeof(Pixel)));
}

// -----------------------------------------------------------------------------
// Sprite sheet file format.
// -----------------------------------------------------------------------------

/// A row of sprites in a sprite sheet.
///
/// Each row in a sprite sheet can have a different number of columns.
///
/// The pixels of the row follow a "sprite-major" order. It contains the
/// 'sprite_width * sprite_height' pixels for the first column/sprite, then the
/// second column/sprite, etc.
///
/// Pixels are 8-bit indices into the sprite sheet's colour palette.
typedef struct Ss_Row {
  uint16_t num_cols;  /// Number of columns in this row.
  uint8_t  pixels[1]; /// Count: num_cols * sprite_width * sprite_height.
} Ss_Row;

typedef struct Ss_Palette {
  uint16_t num_colours;
  Pixel    colours[1]; /// Count: num_colors.
} Ss_Palette;

/// Sprite sheet top-level data definition.
///
/// Sprite width and height are assumed constant throughout the sprite sheet.
typedef struct Ss_SpriteSheet {
  uint16_t   sprite_width;  /// Sprite width in pixels.
  uint16_t   sprite_height; /// Sprite height in pixels.
  uint16_t   num_rows;
  Ss_Palette palette; /// Variable size.
  Ss_Row     rows[1]; /// Count: num_rows. Variable offset.
} Ss_SpriteSheet;

static inline const Ss_Row* get_sprite_sheet_row(
    const Ss_SpriteSheet* sheet, int row) {
  assert(sheet);
  assert(row >= 0);
  assert(row < sheet->num_rows);
  // Skip over the palette.
  const Ss_Row* rows =
      (const Ss_Row*)(&sheet->palette.colours[0] + sheet->palette.num_colours);
  return &rows[row];
}

static inline const uint8_t* get_sprite_sheet_sprite(
    const Ss_SpriteSheet* sheet, const Ss_Row* row, int col) {
  assert(sheet);
  assert(row);
  assert(col >= 0);
  assert(col < row->num_cols);
  const int sprite_offset = col * sheet->sprite_width * sheet->sprite_height;
  const uint8_t* sprite   = &row->pixels[sprite_offset];
  return sprite;
}

// -----------------------------------------------------------------------------
// Renderer state.
// -----------------------------------------------------------------------------

typedef struct TileData {
  uint16_t width;
  uint16_t height;
  uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool.
} TileData;

// File format is already convenient for working in memory.
typedef Ss_Row         SpriteSheetRow;
typedef Ss_SpriteSheet SpriteSheetData;

typedef struct SpriteData {
  SpriteSheet sheet; // Handle to the sprite's sheet.
  ivec2       position;
  int         animation; // Current animation.
  int         frame;     // Current frame of animation.
} SpriteData;

DEF_MEMPOOL_DYN(TilePool, TileData)
DEF_MEM_DYN(PixelPool, Pixel)

DEF_MEMPOOL_DYN(SpritePool, SpriteData)
DEF_MEM_DYN(SpriteSheetPool, SpriteSheetData)

typedef struct IsoGfx {
  int             screen_width;
  int             screen_height;
  int             tile_width;
  int             tile_height;
  int             world_width;
  int             world_height;
  int             max_num_sprites;
  int             sprite_sheet_pool_size_bytes;
  double          last_animation_time;
  Tile*           world;
  Pixel*          screen;
  TilePool        tiles;
  PixelPool       pixels;
  SpritePool      sprites;
  SpriteSheetPool sheets;
} IsoGfx;

// -----------------------------------------------------------------------------
// Math and world / tile / screen access.
// -----------------------------------------------------------------------------

static inline ivec2 ivec2_add(ivec2 a, ivec2 b) {
  return (ivec2){.x = a.x + b.x, .y = a.y + b.y};
}

static inline ivec2 ivec2_scale(ivec2 a, int s) {
  return (ivec2){.x = a.x * s, .y = a.y * s};
}

static inline ivec2 iso2cart(ivec2 iso, int s, int t, int w) {
  return (ivec2){
      .x = (iso.x - iso.y) * (s / 2) + (w / 2), .y = (iso.x + iso.y) * (t / 2)};
}

// Method 1.
// static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
//  const double x    = cart.x - (double)(w / 2);
//  const double xiso = (x * t + cart.y * s) / (double)(s * t);
//  return (vec2){
//      .x = (int)(xiso), .y = (int)((2.0 / (double)t) * cart.y - xiso)};
//}

// Method 2.
static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
  const double one_over_s = 1. / (double)s;
  const double one_over_t = 1. / (double)t;
  const double x          = cart.x - (double)(w / 2);
  return (vec2){
      .x = (one_over_s * x + one_over_t * cart.y),
      .y = (-one_over_s * x + one_over_t * cart.y)};
}

static const Pixel* tile_xy_const_ref(
    const IsoGfx* iso, const TileData* tile, int x, int y) {
  assert(iso);
  assert(tile);
  assert(x >= 0);
  assert(y >= 0);
  assert(x < tile->width);
  assert(y < tile->height);
  return &mem_get_chunk(&iso->pixels, tile->pixels_handle)[y * tile->width + x];
}

// static Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) {
//   return *tile_xy_const_ref(iso, tile, x, y);
// }

static Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) {
  return (Pixel*)tile_xy_const_ref(iso, tile, x, y);
}

static inline const Tile* world_xy_const_ref(const IsoGfx* iso, int x, int y) {
  assert(iso);
  assert(x >= 0);
  assert(y >= 0);
  assert(x < iso->world_width);
  assert(y < iso->world_height);
  return &iso->world[y * iso->world_width + x];
}

static inline Tile world_xy(const IsoGfx* iso, int x, int y) {
  return *world_xy_const_ref(iso, x, y);
}

static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) {
  return (Tile*)world_xy_const_ref(iso, x, y);
}

static inline const Pixel* screen_xy_const_ref(
    const IsoGfx* iso, int x, int y) {
  assert(iso);
  assert(x >= 0);
  assert(y >= 0);
  assert(x < iso->screen_width);
  assert(y < iso->screen_height);
  return &iso->screen[y * iso->screen_width + x];
}

static inline Pixel screen_xy(IsoGfx* iso, int x, int y) {
  return *screen_xy_const_ref(iso, x, y);
}

static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) {
  return (Pixel*)screen_xy_const_ref(iso, x, y);
}

static int calc_num_tile_blocks(
    int base_tile_width, int base_tile_height, int tile_width,
    int tile_height) {
  const int base_tile_size = base_tile_width * base_tile_height;
  const int tile_size      = tile_width * tile_height;
  const int num_blocks     = tile_size / base_tile_size;
  return num_blocks;
}

// -----------------------------------------------------------------------------
// Renderer, world and tile management.
// -----------------------------------------------------------------------------

IsoGfx* isogfx_new(const IsoGfxDesc* desc) {
  assert(desc->screen_width > 0);
  assert(desc->screen_height > 0);
  // Part of our implementation assumes even widths and heights for precision.
  assert((desc->screen_width & 1) == 0);
  assert((desc->screen_height & 1) == 0);

  IsoGfx* iso = calloc(1, sizeof(IsoGfx));
  if (!iso) {
    return 0;
  }

  iso->screen_width  = desc->screen_width;
  iso->screen_height = desc->screen_height;

  iso->last_animation_time = 0.0;

  iso->max_num_sprites = desc->max_num_sprites == 0 ? DEFAULT_MAX_NUM_SPRITES
                                                    : desc->max_num_sprites;
  iso->sprite_sheet_pool_size_bytes = desc->sprite_sheet_pool_size_bytes == 0
                                          ? DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES
                                          : desc->sprite_sheet_pool_size_bytes;

  const int screen_size = desc->screen_width * desc->screen_height;
  if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) {
    goto cleanup;
  }

  return iso;

cleanup:
  isogfx_del(&iso);
  return 0;
}

/// Destroy the world, its tile set, and the underlying pools.
static void destroy_world(IsoGfx* iso) {
  assert(iso);
  if (iso->world) {
    free(iso->world);
    iso->world = 0;
  }
  mempool_del(&iso->tiles);
  mem_del(&iso->pixels);
}

/// Destroy all loaded sprites and the underlying pools.
static void destroy_sprites(IsoGfx* iso) {
  assert(iso);
  mempool_del(&iso->sprites);
  mem_del(&iso->sheets);
}

void isogfx_del(IsoGfx** pIso) {
  assert(pIso);
  IsoGfx* iso = *pIso;
  if (iso) {
    destroy_world(iso);
    destroy_sprites(iso);
    if (iso->screen) {
      free(iso->screen);
      iso->screen = 0;
    }
    free(iso);
    *pIso = 0;
  }
}

bool isogfx_make_world(IsoGfx* iso, const WorldDesc* desc) {
  assert(iso);
  assert(desc);
  assert(desc->tile_width > 0);
  assert(desc->tile_height > 0);
  // Part of our implementation assumes even widths and heights for greater
  // precision.
  assert((desc->tile_width & 1) == 0);
  assert((desc->tile_height & 1) == 0);

  // Handle recreation by destroying the previous world.
  destroy_world(iso);

  iso->tile_width   = desc->tile_width;
  iso->tile_height  = desc->tile_height;
  iso->world_width  = desc->world_width;
  iso->world_height = desc->world_height;

  const int world_size      = desc->world_width * desc->world_height;
  const int tile_size       = desc->tile_width * desc->tile_height;
  const int tile_size_bytes = tile_size * (int)sizeof(Pixel);
  const int tile_pool_size =
      desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES;

  if (!(iso->world = calloc(world_size, sizeof(Tile)))) {
    goto cleanup;
  }
  if (!mempool_make_dyn(&iso->tiles, world_size, sizeof(TileData))) {
    goto cleanup;
  }
  if (!mem_make_dyn(&iso->pixels, tile_pool_size, tile_size_bytes)) {
    goto cleanup;
  }

  return true;

cleanup:
  destroy_world(iso);
  return false;
}

bool isogfx_load_world(IsoGfx* iso, const char* filepath) {
  assert(iso);
  assert(filepath);

  bool success = false;

  // Handle recreation by destroying the previous world.
  destroy_world(iso);

  // Load the map.
  printf("Load tile map: %s\n", filepath);
  Tm_Map* map = read_file(filepath);
  if (!map) {
    goto cleanup;
  }

  // Allocate memory for the map and tile sets.
  const int world_size           = map->world_width * map->world_height;
  const int base_tile_size       = map->base_tile_width * map->base_tile_height;
  const int base_tile_size_bytes = base_tile_size * (int)sizeof(Pixel);
  // TODO: Need to get the total number of tiles from the map.
  const int tile_pool_size = DEFAULT_MAX_NUM_TILES;

  if (!(iso->world = calloc(world_size, sizeof(Tile)))) {
    goto cleanup;
  }
  if (!mempool_make_dyn(&iso->tiles, tile_pool_size, sizeof(TileData))) {
    goto cleanup;
  }
  if (!mem_make_dyn(&iso->pixels, tile_pool_size, base_tile_size_bytes)) {
    goto cleanup;
  }

  // Load the tile sets.
  const Tm_Layer* layer = &map->layers[0];
  // TODO: Handle num_layers layers.
  for (int i = 0; i < 1; ++i) {
    const char* ts_path = layer->tileset_path;

    // Tile set path is relative to the tile map file. Make it relative to the
    // current working directory before loading.
    char ts_path_cwd[PATH_MAX] = {0};
    if (!path_make_relative(filepath, ts_path, ts_path_cwd, PATH_MAX)) {
      goto cleanup;
    }

    Ts_TileSet* tileset = read_file(ts_path_cwd);
    if (!tileset) {
      goto cleanup;
    };

    // Load tile data.
    const Ts_Tile* tile = &tileset->tiles[0];
    for (uint16_t j = 0; j < tileset->num_tiles; ++j) {
      // Tile dimensions should be a multiple of the base tile size.
      assert((tile->width % map->base_tile_width) == 0);
      assert((tile->height % map->base_tile_height) == 0);

      // Allocate N base tile size blocks for the tile.
      const uint16_t tile_size  = tile->width * tile->height;
      const int      num_blocks = tile_size / base_tile_size;
      Pixel*         pixels     = mem_alloc(&iso->pixels, num_blocks);
      assert(pixels);
      memcpy(pixels, tile->pixels, tile_size * sizeof(Pixel));

      // Allocate the tile data.
      TileData* tile_data = mempool_alloc(&iso->tiles);
      assert(tile_data);
      tile_data->width  = tile->width;
      tile_data->height = tile->height;
      tile_data->pixels_handle =
          (uint16_t)mem_get_chunk_handle(&iso->pixels, pixels);

      tile = ts_tileset_get_next_tile(tileset, tile);
    }

    printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd);

    free(tileset);
    layer = tm_map_get_next_layer(map, layer);
  }

  // Load the map into the world.
  layer = &map->layers[0];
  // TODO: Handle num_layers layers.
  for (int i = 0; i < 1; ++i) {
    memcpy(iso->world, layer->tiles, world_size * sizeof(Tile));

    // TODO: We need to handle 'firsgid' in TMX files.
    for (int j = 0; j < world_size; ++j) {
      iso->world[j] -= 1;
    }

    layer = tm_map_get_next_layer(map, layer);
  }

  iso->world_width  = map->world_width;
  iso->world_height = map->world_height;
  iso->tile_width   = map->base_tile_width;
  iso->tile_height  = map->base_tile_height;

  success = true;

cleanup:
  if (map) {
    free(map);
  }
  if (!success) {
    destroy_world(iso);
  }
  return success;
}

int isogfx_world_width(const IsoGfx* iso) {
  assert(iso);
  return iso->world_width;
}

int isogfx_world_height(const IsoGfx* iso) {
  assert(iso);
  return iso->world_height;
}

/// Create a tile mask procedurally.
static void make_tile_from_colour(
    const IsoGfx* iso, Pixel colour, TileData* tile) {
  assert(iso);
  assert(tile);

  const int width  = tile->width;
  const int height = tile->height;
  const int r      = width / height;

  for (int y = 0; y < height / 2; ++y) {
    const int mask_start = width / 2 - r * y - 1;
    const int mask_end   = width / 2 + r * y + 1;
    for (int x = 0; x < width; ++x) {
      const bool  mask = (mask_start <= x) && (x <= mask_end);
      const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0};

      // Top half.
      *tile_xy_mut(iso, tile, x, y) = val;

      // Bottom half reflects the top half.
      const int y_reflected                   = height - y - 1;
      *tile_xy_mut(iso, tile, x, y_reflected) = val;
    }
  }
}

Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) {
  assert(iso);
  assert(desc);
  // Client must create world before creating tiles.
  assert(iso->tile_width > 0);
  assert(iso->tile_height > 0);

  TileData* tile = mempool_alloc(&iso->tiles);
  assert(tile); // TODO: Make this a hard assert.

  const int num_blocks = calc_num_tile_blocks(
      iso->tile_width, iso->tile_height, desc->width, desc->height);

  Pixel* pixels = mem_alloc(&iso->pixels, num_blocks);
  assert(pixels); // TODO: Make this a hard assert.

  tile->width         = desc->width;
  tile->height        = desc->height;
  tile->pixels_handle = mem_get_chunk_handle(&iso->pixels, pixels);

  switch (desc->type) {
  case TileFromColour:
    make_tile_from_colour(iso, desc->colour, tile);
    break;
  case TileFromFile:
    assert(false); // TODO
    break;
  case TileFromMemory:
    assert(false); // TODO
    break;
  }

  return (Tile)mempool_get_block_index(&iso->tiles, tile);
}

void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) {
  assert(iso);
  *world_xy_mut(iso, x, y) = tile;
}

void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) {
  assert(iso);
  for (int y = y0; y < y1; ++y) {
    for (int x = x0; x < x1; ++x) {
      isogfx_set_tile(iso, x, y, tile);
    }
  }
}

bool isogfx_load_sprite_sheet(
    IsoGfx* iso, const char* filepath, SpriteSheet* p_sheet) {
  assert(iso);
  assert(filepath);
  assert(p_sheet);

  bool success = false;

  // Lazy initialization of sprite pools.
  if (mempool_capacity(&iso->sprites) == 0) {
    if (!mempool_make_dyn(
            &iso->sprites, iso->max_num_sprites, sizeof(SpriteData))) {
      return false;
    }
  }
  if (mem_capacity(&iso->sheets) == 0) {
    // Using a block size of 1 byte for sprite sheet data.
    if (!mem_make_dyn(&iso->sheets, iso->sprite_sheet_pool_size_bytes, 1)) {
      return false;
    }
  }

  // Load sprite sheet file.
  printf("Load sprite sheet: %s\n", filepath);
  FILE* file = fopen(filepath, "rb");
  if (file == NULL) {
    goto cleanup;
  }
  const size_t     sheet_size = get_file_size(file);
  SpriteSheetData* ss_sheet   = mem_alloc(&iso->sheets, sheet_size);
  if (!ss_sheet) {
    goto cleanup;
  }
  if (fread(ss_sheet, sheet_size, 1, file) != 1) {
    goto cleanup;
  }

  *p_sheet = mem_get_chunk_handle(&iso->sheets, ss_sheet);
  success  = true;

cleanup:
  // Pools remain initialized since client may attempt to load other sprites.
  if (file != NULL) {
    fclose(file);
  }
  if (!success) {
    if (ss_sheet) {
      mem_free(&iso->sheets, &ss_sheet);
    }
  }
  return success;
}

Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) {
  assert(iso);

  SpriteData* sprite = mempool_alloc(&iso->sprites);
  assert(sprite);

  sprite->sheet = sheet;

  return mempool_get_block_index(&iso->sprites, sprite);
}

#define with_sprite(SPRITE, BODY)                                \
  {                                                              \
    SpriteData* data = mempool_get_block(&iso->sprites, sprite); \
    assert(data);                                                \
    BODY;                                                        \
  }

void isogfx_set_sprite_position(IsoGfx* iso, Sprite sprite, int x, int y) {
  assert(iso);
  with_sprite(sprite, {
    data->position.x = x;
    data->position.y = y;
  });
}

void isogfx_set_sprite_animation(IsoGfx* iso, Sprite sprite, int animation) {
  assert(iso);
  with_sprite(sprite, { data->animation = animation; });
}

void isogfx_update(IsoGfx* iso, double t) {
  assert(iso);

  // If this is the first time update() is called after initialization, just
  // record the starting animation time.
  if (iso->last_animation_time == 0.0) {
    iso->last_animation_time = t;
    return;
  }

  if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) {
    // TODO: Consider linking animated sprites in a list so that we only walk
    // over those here and not also the static sprites.
    mempool_foreach(&iso->sprites, sprite, {
      const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet);
      assert(sheet); // TODO: Make this a hard assert inside the mem/pool.
      const SpriteSheetRow* row =
          get_sprite_sheet_row(sheet, sprite->animation);
      sprite->frame = (sprite->frame + 1) % row->num_cols;
    });

    iso->last_animation_time = t;
  }
}

// -----------------------------------------------------------------------------
// Rendering and picking.
// -----------------------------------------------------------------------------

typedef struct CoordSystem {
  ivec2 o; /// Origin.
  ivec2 x;
  ivec2 y;
} CoordSystem;

/// Create the basis for the isometric coordinate system with origin and vectors
/// expressed in the Cartesian system.
static CoordSystem make_iso_coord_system(const IsoGfx* iso) {
  assert(iso);
  const ivec2 o = {iso->screen_width / 2, 0};
  const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
  const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
  return (CoordSystem){o, x, y};
}

/// Get the screen position of the top diamond-corner of the tile at world
/// (x,y).
static ivec2 GetTileScreenOrigin(
    const CoordSystem iso_space, int world_x, int world_y) {
  const ivec2 vx_offset = ivec2_scale(iso_space.x, world_x);
  const ivec2 vy_offset = ivec2_scale(iso_space.y, world_y);
  const ivec2 screen_origin =
      ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset));

  return screen_origin;
}

static Pixel alpha_blend(Pixel src, Pixel dst) {
  if ((src.a == 255) || (dst.a == 0)) {
    return src;
  }
  const uint16_t one_minus_alpha = 255 - src.a;
#define blend(s, d)                             \
  (Channel)(                                    \
      (double)((uint16_t)s * (uint16_t)src.a +  \
               (uint16_t)d * one_minus_alpha) / \
      255.0)
  return (Pixel){
      .r = blend(src.r, dst.r),
      .g = blend(src.g, dst.g),
      .b = blend(src.b, dst.b),
      .a = src.a};
}

/// Draw a rectangle (tile or sprite).
///
/// The rectangle's bottom-left corner is mapped to the given origin. The
/// rectangle then extends to the right and top of the origin.
///
/// The rectangle's pixels are assumed to be arranged in a linear, row-major
/// fashion.
///
/// If indices are given, then the image is assumed to be colour-paletted, where
/// 'pixels' is the palette and 'indices' the pixel indices. Otherwise, the
/// image is assumed to be in plain RGBA format.
static void draw_rect(
    IsoGfx* iso, ivec2 top_left, int rect_width, int rect_height,
    const Pixel* pixels, const uint8_t* indices) {
  assert(iso);

#define rect_pixel(x, y)                           \
  (indices ? pixels[indices[py * rect_width + px]] \
           : pixels[py * rect_width + px])

  // Rect origin can be outside screen bounds, so we must offset accordingly to
  // draw only the visible portion.
#define max(a, b) (a > b ? a : b)
  const int px_offset = max(0, -top_left.x);
  const int py_offset = max(0, -top_left.y);
  // Adjust top-left to be inside bounds.
  top_left.x = max(0, top_left.x);
  top_left.y = max(0, top_left.y);

  // Rect can exceed screen bounds, so clip along Y and X as we draw.
  for (int py = py_offset;
       (py < rect_height) && (top_left.y + py < iso->screen_height); ++py) {
    const int sy = top_left.y + py - py_offset;
    for (int px = px_offset;
         (px < rect_width) && (top_left.x + px < iso->screen_width); ++px) {
      const Pixel colour = rect_pixel(px, py);
      if (colour.a > 0) {
        const int   sx              = top_left.x + px;
        const Pixel dst             = screen_xy(iso, sx, sy);
        const Pixel final           = alpha_blend(colour, dst);
        *screen_xy_mut(iso, sx, sy) = final;
      }
    }
  }
}

/// Draw a tile.
///
/// 'screen_origin' is the screen coordinates of the top diamond-corner of the
/// tile (the base tile for super tiles).
///     World (0, 0) -> (screen_width / 2, 0).
static void draw_tile(IsoGfx* iso, ivec2 screen_origin, Tile tile) {
  assert(iso);

  const TileData* tile_data = mempool_get_block(&iso->tiles, tile);
  assert(tile_data);
  const Pixel* pixels = tile_xy_const_ref(iso, tile_data, 0, 0);

  // Move from the top diamond-corner to the top-left corner of the tile image.
  // For regular tiles, tile height == base tile height, so the y offset is 0.
  // For super tiles, move as high up as the height of the tile.
  const ivec2 offset = {
      -(iso->tile_width / 2), tile_data->height - iso->tile_height};
  const ivec2 top_left = ivec2_add(screen_origin, offset);

  draw_rect(iso, top_left, tile_data->width, tile_data->height, pixels, 0);
}

static void draw_world(IsoGfx* iso) {
  assert(iso);

  const int W = iso->screen_width;
  const int H = iso->screen_height;

  memset(iso->screen, 0, W * H * sizeof(Pixel));

  const CoordSystem iso_space = make_iso_coord_system(iso);

  // TODO: Culling.
  // Ex: map the screen corners to tile space to cull.
  // Ex: walk in screen space and fetch the tile.
  // The tile-centric approach might be more cache-friendly since the
  // screen-centric approach would juggle multiple tiles throughout the scan.
  for (int wy = 0; wy < iso->world_height; ++wy) {
    for (int wx = 0; wx < iso->world_width; ++wx) {
      const Tile  tile          = world_xy(iso, wx, wy);
      const ivec2 screen_origin = GetTileScreenOrigin(iso_space, wx, wy);
      draw_tile(iso, screen_origin, tile);
    }
  }
}

static void draw_sprite(
    IsoGfx* iso, ivec2 origin, const SpriteData* sprite,
    const SpriteSheetData* sheet) {
  assert(iso);
  assert(sprite);
  assert(sheet);
  assert(sprite->animation >= 0);
  assert(sprite->animation < sheet->num_rows);
  assert(sprite->frame >= 0);

  const SpriteSheetRow* row = get_sprite_sheet_row(sheet, sprite->animation);
  const uint8_t* frame = get_sprite_sheet_sprite(sheet, row, sprite->frame);
  draw_rect(
      iso, origin, sheet->sprite_width, sheet->sprite_height,
      sheet->palette.colours, frame);
}

static void draw_sprites(IsoGfx* iso) {
  assert(iso);

  const CoordSystem iso_space = make_iso_coord_system(iso);

  mempool_foreach(&iso->sprites, sprite, {
    const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet);
    assert(sheet);

    const ivec2 screen_origin =
        GetTileScreenOrigin(iso_space, sprite->position.x, sprite->position.y);
    draw_sprite(iso, screen_origin, sprite, sheet);
  });
}

void isogfx_render(IsoGfx* iso) {
  assert(iso);
  draw_world(iso);
  draw_sprites(iso);
}

void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) {
  assert(iso);
  assert(x >= 0);
  assert(y >= 0);
  assert(x < iso->world_width);
  assert(y < iso->world_height);

  const CoordSystem iso_space     = make_iso_coord_system(iso);
  const ivec2       screen_origin = GetTileScreenOrigin(iso_space, x, y);
  draw_tile(iso, screen_origin, tile);
}

bool isogfx_resize(IsoGfx* iso, int screen_width, int screen_height) {
  assert(iso);
  assert(iso->screen);

  const int current_size = iso->screen_width * iso->screen_height;
  const int new_size     = screen_width * screen_height;

  if (new_size > current_size) {
    Pixel* new_screen = calloc(new_size, sizeof(Pixel));
    if (new_screen) {
      free(iso->screen);
      iso->screen = new_screen;
    } else {
      return false;
    }
  }
  iso->screen_width  = screen_width;
  iso->screen_height = screen_height;
  return true;
}

const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) {
  assert(iso);
  return iso->screen;
}

void isogfx_pick_tile(
    const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) {
  assert(iso);
  assert(xiso);
  assert(yiso);

  const vec2 xy_iso = cart2iso(
      (vec2){.x = xcart, .y = ycart}, iso->tile_width, iso->tile_height,
      iso->screen_width);

  if ((0 <= xy_iso.x) && (xy_iso.x < iso->world_width) && (0 <= xy_iso.y) &&
      (xy_iso.y < iso->world_height)) {
    *xiso = (int)xy_iso.x;
    *yiso = (int)xy_iso.y;
  } else {
    *xiso = -1;
    *yiso = -1;
  }
}