/**
 * @file tilemap.c
 * @brief Implements the tilemap subsystem.
 */

#include <string.h>
#include <libgen.h>

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <jansson.h>

#include <akerror.h>
#include <akstdlib.h>

#include <akgl/tilemap.h>
#include <akgl/actor.h>
#include <akgl/json_helpers.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/game.h>
#include <akgl/util.h>

akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest)
{
    PREPARE_ERROR(errctx);
    json_t *properties = NULL;
    json_t *property = NULL;
    akgl_String *tmpstr = NULL;
    akgl_String *typestr = NULL;
    bool found = false;
    int i = 0;
    // This is not a generic JSON helper. It assumes we are receiving an object with a 'properties' key
    // inside of it. That key is an array of objects, and each object has a name, type, and value.
    FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL json obj reference");
    FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
    ATTEMPT {
	CATCH(errctx, akgl_get_json_array_value(obj, "properties", &properties));
	for (i = 0; i < json_array_size(properties); i++) {
	    CATCH(errctx, akgl_get_json_array_index_object(properties, i, &property));
	    // One scratch string for the whole scan. akgl_get_json_string_value
	    // reuses a non-NULL *dest without taking another reference, so
	    // releasing it per iteration -- which this used to do -- dropped the
	    // refcount to zero while the slot was still in use, and the next
	    // claim anywhere could have been handed the same one.
	    CATCH(errctx, akgl_get_json_string_value(property, "name", &tmpstr));
	    if ( strcmp(tmpstr->data, key) != 0 ) {
		continue;
	    }
	    CATCH(errctx, akgl_get_json_string_value(property, "type", &typestr));
	    if ( strcmp(typestr->data, type) != 0 ) {
		FAIL_BREAK(errctx, AKERR_TYPE, "Property %s is present but is incorrect type(expected %s got %s)", key, type, (char *)typestr->data);
	    }
	    *dest = property;
	    found = true;
	    break;
	}
    } CLEANUP {
	if ( tmpstr != NULL ) {
	    IGNORE(akgl_heap_release_string(tmpstr));
	}
	if ( typestr != NULL ) {
	    IGNORE(akgl_heap_release_string(typestr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    // The success path used to be a SUCCEED_RETURN from inside the ATTEMPT
    // block, which returned past CLEANUP and leaked both scratch strings on
    // every successful lookup. The pool is 256 entries and a map load does this
    // many times per layer.
    if ( found == true ) {
	SUCCEED_RETURN(errctx);
    }
    FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
}

akerr_ErrorContext *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest)
{
    PREPARE_ERROR(errctx);
    json_t *property;

    PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
    PASS(errctx, akgl_heap_next_string(dest));
    PASS(errctx, akgl_string_initialize(*dest, NULL));
    PASS(errctx, akgl_get_json_string_value(property, "value", dest));

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_get_json_properties_boolean(json_t *obj, char *key, bool *dest)
{
    PREPARE_ERROR(errctx);
    json_t *property = NULL;
    PASS(errctx, akgl_get_json_tilemap_property(obj, key, "bool", &property));
    PASS(errctx, akgl_get_json_boolean_value(property, "value", dest));

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest)
{
    PREPARE_ERROR(errctx);
    json_t *property = NULL;
    PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
    PASS(errctx, akgl_get_json_integer_value(property, "value", dest));

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float32_t *dest)
{
    PREPARE_ERROR(errctx);
    json_t *property = NULL;
    PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
    PASS(errctx, akgl_get_json_number_value(property, "value", dest));

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float32_t *dest)
{
    PREPARE_ERROR(errctx);
    json_t *property = NULL;
    PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
    PASS(errctx, akgl_get_json_number_value(property, "value", dest));

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, float64_t *dest)
{
    PREPARE_ERROR(errctx);
    json_t *property = NULL;
    PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
    PASS(errctx, akgl_get_json_double_value(property, "value", dest));

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname)
{
    PREPARE_ERROR(errctx);
    akgl_String *tmpstr = NULL;
    akgl_String *tmppath = NULL;

    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "imagewidth", &dest->tilesets[tsidx].imagewidth));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "margin", &dest->tilesets[tsidx].margin));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "spacing", &dest->tilesets[tsidx].spacing));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tilecount", &dest->tilesets[tsidx].tilecount));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tileheight", &dest->tilesets[tsidx].tileheight));
    PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tilewidth", &dest->tilesets[tsidx].tilewidth));

    PASS(errctx, akgl_get_json_string_value((json_t *)tileset, "name", &tmpstr));
    PASS(errctx, akgl_heap_next_string(&tmppath));
    ATTEMPT {
	CATCH(errctx, aksl_strncpy(
		  (char *)&dest->tilesets[tsidx].name,
		  sizeof(dest->tilesets[tsidx].name),
		  (char *)&tmpstr->data,
		  sizeof(dest->tilesets[tsidx].name) - 1
		  ));

	CATCH(errctx, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
	CATCH(errctx, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath));
	CATCH(errctx, aksl_strncpy(
		  (char *)&dest->tilesets[tsidx].imagefilename,
		  sizeof(dest->tilesets[tsidx].imagefilename),
		  tmppath->data,
		  sizeof(dest->tilesets[tsidx].imagefilename) - 1
		  ));
    } CLEANUP {
	IGNORE(akgl_heap_release_string(tmpstr));
	IGNORE(akgl_heap_release_string(tmppath));
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    dest->tilesets[tsidx].texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
    FAIL_ZERO_RETURN(errctx, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx)
{
    int x_offset = 0;
    int y_offset = 0;
    int x_col = 0;
    int y_col = 0;
    int j = 0;
    PREPARE_ERROR(errctx);
    /* FIXME: THIS DOES NOT PROPERLY ACCOUNT FOR MARGINS
     * It should be possible to make it work easily I just didn't feel like accounting for them in the
     * initial math.
     */
    /*SDL_Log("Tileset %s has %d rows %d columns",
      dest->tilesets[tilesetidx].name,
      (dest->tilesets[tilesetidx].tilecount / dest->tilesets[tilesetidx].columns),
      dest->tilesets[tilesetidx].columns);*/
    for (j = 0; j <= (dest->tilesets[tilesetidx].tilecount); j++) {
	/*
	 * For a given 8x2 tilemap like this with 10x10 tiles and 0 spacing and 0 margin
	 *
	 * 01234567
	 * 89ABCDEF
	 *
	 * tile 0 would be offset (0,0)
	 * tile 4 would be offset (40,1)
	 * tile 7 would be offset (70,1)
	 * tile 8 would be offset (1,8)
	 * tile C would be offset (40,8)
	 * tile F would be offset (70,8)
	 */
	if ( j >= dest->tilesets[tilesetidx].columns ) {
	    x_col = (j % dest->tilesets[tilesetidx].columns);
	    y_col = (j / dest->tilesets[tilesetidx].columns);
	    x_offset = x_col * (dest->tilesets[tilesetidx].tilewidth + dest->tilesets[tilesetidx].spacing);
	    y_offset = y_col * (dest->tilesets[tilesetidx].tileheight + dest->tilesets[tilesetidx].spacing);
	} else {
	    x_col = j;
	    y_col = 0;
	    x_offset = (j * (dest->tilesets[tilesetidx].tilewidth + dest->tilesets[tilesetidx].spacing));
	    y_offset = dest->tilesets[tilesetidx].spacing;
	}

	dest->tilesets[tilesetidx].tile_offsets[j][0] = x_offset;
	dest->tilesets[tilesetidx].tile_offsets[j][1] = y_offset;
	/* SDL_Log("Tileset %s index (%d, %d) is offset (%d, %d)",
	   dest->tilesets[tilesetidx].name,
	   x_col,
	   y_col,
	   x_offset,
	   y_offset);*/
	// SDL_Log("Processed %d total tiles for tileset", j);
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "Received NULL tilemap pointer");
    FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "Received NULL json object pointer");

    json_t *tilesets = NULL;
    json_t *jstileset = NULL;
    int i;

    dest->numtilesets = 0;
    ATTEMPT {
	CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
	    for (i = 0; i < json_array_size((json_t *)tilesets); i++) {
		// The bound goes at the top of the body, matching
		// akgl_tilemap_load_layers. Without it a map with seventeen
		// tilesets wrote past the fixed table -- and unlike the layer
		// count, nothing anywhere else was checking this one.
		FAIL_NONZERO_BREAK(
		    errctx,
		    (i >= AKGL_TILEMAP_MAX_TILESETS),
		    AKERR_OUTOFBOUNDS,
		    "Map declares more than %d tilesets",
		    AKGL_TILEMAP_MAX_TILESETS
		    );
		CATCH(errctx, akgl_get_json_array_index_object((json_t *)tilesets, i, &jstileset));
		CATCH(errctx, akgl_tilemap_load_tilesets_each(jstileset, dest, i, dirname));
		CATCH(errctx, akgl_tilemap_compute_tileset_offsets(dest, i));
		dest->numtilesets += 1;
	    }
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
{
    PREPARE_ERROR(errctx);
    akgl_String *tmpstr = NULL;
    akgl_Actor *actorobj = NULL;

    curobj->type = AKGL_TILEMAP_OBJECT_TYPE_ACTOR;
    if ( strlen((char *)&curobj->name) == 0 ) {
	FAIL_RETURN(errctx, AKERR_KEY, "Actor in tile object layer cannot have empty name");
    }

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&tmpstr));
	actorobj = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, (char *)&curobj->name, NULL);
	if ( actorobj == NULL ) {
	    CATCH(errctx, akgl_heap_next_actor(&actorobj));
	    CATCH(errctx, akgl_actor_initialize((akgl_Actor *)actorobj, (char *)&curobj->name));
	    CATCH(errctx, akgl_get_json_properties_string((json_t *)layerdatavalue, "character", &tmpstr));
	    CATCH(errctx,
		  akgl_actor_set_character(
		      (akgl_Actor *)actorobj,
		      (char *)&tmpstr->data
		      )
		);
	} else {
	    actorobj->refcount += 1;
	}
	CATCH(errctx, akgl_get_json_properties_integer((json_t *)layerdatavalue, "state", &actorobj->state));
    } CLEANUP {
	if ( tmpstr != NULL ) {
	    IGNORE(akgl_heap_release_string(tmpstr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    actorobj->layer = layerid;
    actorobj->x = curobj->x;
    actorobj->y = curobj->y;
    actorobj->visible = curobj->visible;
    curobj->actorptr = (akgl_Actor *)actorobj;

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
    PREPARE_ERROR(errctx);
    json_t *layerdata = NULL;
    json_t *layerdatavalue = NULL;
    int j;
    int len;
    akgl_TilemapLayer *curlayer = NULL;
    akgl_TilemapObject *curobj = NULL;
    akgl_String *tmpstr = NULL;

    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
    FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");

    PASS(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
    len = json_array_size((json_t *)layerdata);
    curlayer = &dest->layers[layerid];

    // The loop is the whole ATTEMPT body on purpose. A CATCH inside a loop
    // breaks the loop rather than the block, which is only safe because there
    // is nothing after it -- CLEANUP, PROCESS and FINISH still run and still
    // propagate. Anything added after this loop has to account for that.
    ATTEMPT {
	for ( j = 0; j < len; j++ ) {
	    FAIL_NONZERO_BREAK(
		errctx,
		(j >= AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER),
		AKERR_OUTOFBOUNDS,
		"Object layer %d has more than %d objects",
		layerid,
		AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER
		);
	    CATCH(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
	    curobj = &curlayer->objects[j];
	    // One scratch string for the whole walk, released once in CLEANUP.
	    // Releasing it after the name read -- which is what this used to do
	    // -- dropped its refcount to zero while the very next line went on
	    // using the same slot, because akgl_get_json_string_value reuses a
	    // non-NULL destination without taking another reference. Any other
	    // claim in between would have been handed the same string.
	    CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
	    CATCH(errctx, aksl_strncpy(
		      (char *)curobj->name,
		      sizeof(curobj->name),
		      tmpstr->data,
		      sizeof(curobj->name) - 1
		      ));
	    CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
	    CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
	    CATCH(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));

	    CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
	    if ( strcmp(tmpstr->data, "actor") == 0 ) {
		CATCH(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
	    } else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
		curobj->visible = false;
		if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
		    dest->p_foreground_y = curobj->y;
		    CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
		    CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
		} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
		    dest->p_vanishing_y = curobj->y;
		    CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
		    CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
		}
	    }
	    layerdatavalue = NULL;
	}
    } CLEANUP {
	if ( tmpstr != NULL ) {
	    IGNORE(akgl_heap_release_string(tmpstr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
    PREPARE_ERROR(errctx);
    json_t *layerdata = NULL;
    bool collidable = false;
    int j;
    int layerdatalen;

    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
    FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
    FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");

    ATTEMPT {
	CATCH(errctx, akgl_get_json_integer_value(root, "height", &dest->layers[layerid].height));
	CATCH(errctx, akgl_get_json_integer_value(root, "width", &dest->layers[layerid].width));
	CATCH(errctx, akgl_get_json_array_value(root, "data", &layerdata));
	layerdatalen = (dest->layers[layerid].width * dest->layers[layerid].height);
	if ( layerdatalen >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
	    FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map layer exceeds the maximum size");
	}
	for ( j = 0; j < layerdatalen; j++ ) {
	    CATCH(errctx, akgl_get_json_array_index_integer(layerdata, j, &dest->layers[layerid].data[j]));
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    /*
     * Which layers are solid is the map's business, not the game's. Before this
     * a game had to hard-code a layer index -- both example games do, because
     * akgl_TilemapLayer does not retain the name Tiled wrote -- and that index
     * changes the moment somebody reorders layers in the editor.
     *
     * A layer with no `collidable` property is not solid. That is the common
     * case, it is not an error, and it is why this is its own ATTEMPT block with
     * a HANDLE rather than a CATCH in the one above.
     */
    ATTEMPT {
	CATCH(errctx, akgl_get_json_properties_boolean(root, "collidable", &collidable));
	if ( collidable == true ) {
	    dest->collidablelayers |= (1u << layerid);
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_KEY) {
	// No such property. Not solid, and not a complaint.
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
    PREPARE_ERROR(errctx);
    akgl_String *tmpstr;
    akgl_String *fpath;
    int fpathlen = 0;

    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
    FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
    FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&tmpstr));
	CATCH(errctx, akgl_heap_next_string(&fpath));
	CATCH(errctx, akgl_get_json_string_value(root, "image", &tmpstr));

	// aksl_snprintf rather than snprintf(3) under a silenced
	// -Wformat-truncation: the compiler was right that "%s/%s" of two
	// AKGL_MAX_STRING_LENGTH strings does not fit, and silencing it left
	// the truncation happening and unreported. What followed was
	// IMG_LoadTexture on a path that was almost the right one, failing with
	// SDL's "couldn't open" against a filename the caller never wrote.
	// aksl_snprintf raises AKERR_OUTOFBOUNDS naming both lengths instead.
	CATCH(errctx, aksl_snprintf(
		  &fpathlen,
		  (char *)&fpath->data,
		  AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
		  "%s/%s",
		  dirname->data,
		  tmpstr->data
		  ));

	dest->layers[layerid].texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)fpath->data);
	FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError());
	dest->layers[layerid].width = dest->layers[layerid].texture->w;
	dest->layers[layerid].height = dest->layers[layerid].texture->h;
    } CLEANUP {
	IGNORE(akgl_heap_release_string(tmpstr));
	IGNORE(akgl_heap_release_string(fpath));
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "akgl_tilemap_load_layers received NULL tilemap pointer");
    FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "akgl_tilemap_load_layers received NULL json object pointer");
    FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");
    json_t *layers = NULL;
    json_t *layer = NULL;
    akgl_String *tmpstr = NULL;
    int i;
    int layerid = 0;
    int tmpint = 0;

    ATTEMPT {
	CATCH(errctx, akgl_get_json_array_value(root, "layers", &layers));
	dest->numlayers = json_array_size((json_t *)layers);
	for ( i = 0; i < dest->numlayers; i++) {
	    if ( i >= AKGL_TILEMAP_MAX_LAYERS ) {
		FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum number of layers");
	    }
	    CATCH(errctx, akgl_get_json_array_index_object((json_t *)layers, i, &layer));
	    CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "id", &tmpint));
	    CATCH(errctx, akgl_get_json_number_value((json_t *)layer, "opacity", &dest->layers[layerid].opacity));
	    CATCH(errctx, akgl_get_json_boolean_value((json_t *)layer, "visible", &dest->layers[layerid].visible));
	    CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "id", &dest->layers[layerid].id));
	    CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "x", &dest->layers[layerid].x));
	    CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "y", &dest->layers[layerid].y));

	    CATCH(errctx, akgl_get_json_string_value((json_t *)layer, "type", &tmpstr));
	    SDL_Log("Layer %d has type %s", layerid, tmpstr->data);
	    if ( strncmp((char *)tmpstr->data, "objectgroup", strlen((char *)tmpstr->data)) == 0 ) {
		dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_OBJECTS;
		CATCH(errctx, akgl_tilemap_load_layer_objects((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
	    } else if ( strncmp((char *)tmpstr->data, "tilelayer", strlen((char *)tmpstr->data)) == 0 ) {
		dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
		CATCH(errctx, akgl_tilemap_load_layer_tile((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
	    } else if ( strncmp((char *)tmpstr->data, "imagelayer", strlen((char *)tmpstr->data)) == 0 ) {
		dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_IMAGE;
		CATCH(errctx, akgl_tilemap_load_layer_image((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
	    }
	    layer = NULL;
	    layerid += 1;
	}
    } CLEANUP {
	// One scratch string for every layer's type, given back once. It was
	// claimed by the first accessor call and never released, which is one
	// of the two pool strings a map load used to keep for good.
	if ( tmpstr != NULL ) {
	    IGNORE(akgl_heap_release_string(tmpstr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
{
    PREPARE_ERROR(errctx);
    json_t *props = NULL;
    akgl_String *tmpval = NULL;
    double defzero = 0.0;

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&tmpval));
	CATCH(errctx, akgl_get_json_array_value((json_t *)root, "properties", &props));
    } CLEANUP {
	IGNORE(akgl_heap_release_string(tmpval));
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_KEY) {
	// Map has no properties, do nothing
	SDL_Log("Map has no properties");
	SUCCEED_RETURN(errctx);
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&tmpval));
	CATCH(errctx, akgl_get_json_properties_string(
		  root,
		  "physics.model",
		  &tmpval
		  )
	    );
	PASS(errctx, akgl_physics_factory(&dest->physics, tmpval));
	dest->use_own_physics = true;

	CATCH(errctx, akgl_get_json_with_default(
		  akgl_get_json_properties_double(
		      root, "physics.gravity.x", &dest->physics.gravity_x
		      ),
		  (void *)&defzero,
		  (void *)&dest->physics.gravity_x,
		  sizeof(double)));
	CATCH(errctx, akgl_get_json_with_default(
		  akgl_get_json_properties_double(
		      root, "physics.gravity.y", &dest->physics.gravity_y
		      ),
		  (void *)&defzero,
		  (void *)&dest->physics.gravity_y,
		  sizeof(double)));
	CATCH(errctx, akgl_get_json_with_default(
		  akgl_get_json_properties_double(
		      root, "physics.gravity.z", &dest->physics.gravity_z
		      ),
		  (void *)&defzero,
		  (void *)&dest->physics.gravity_z,
		  sizeof(double)));
	CATCH(errctx, akgl_get_json_with_default(
		  akgl_get_json_properties_double(
		      root, "physics.drag.x", &dest->physics.drag_x
		      ),
		  (void *)&defzero,
		  (void *)&dest->physics.drag_x,
		  sizeof(double)));
	CATCH(errctx, akgl_get_json_with_default(
		  akgl_get_json_properties_double(
		      root, "physics.drag.y", &dest->physics.drag_y
		      ),
		  (void *)&defzero,
		  (void *)&dest->physics.drag_y,
		  sizeof(double)));
	CATCH(errctx, akgl_get_json_with_default(
		  akgl_get_json_properties_double(
		      root, "physics.drag.z", &dest->physics.drag_z
		      ),
		  (void *)&defzero,
		  (void *)&dest->physics.drag_z,
		  sizeof(double)));
    } CLEANUP {
	IGNORE(akgl_heap_release_string(tmpval));
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_KEY) {
	SDL_Log("Map uses game physics");
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
{
    PREPARE_ERROR(errctx);
    json_t *json = NULL;
    //akgl_String *tmpstr = NULL;
    json_error_t error;
    akgl_String *dirnamestr = NULL;

    FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "load_tilemap received null filename");
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "load_tilemap received null tilemap");

    memset(dest, 0x00, sizeof(akgl_Tilemap));
    dest->p_foreground_scale = 1.0;
    dest->p_vanishing_scale = 1.0;

    PASS(errctx, akgl_heap_next_string(&dirnamestr));
    ATTEMPT {
	//CATCH(errctx, akgl_heap_next_string(&tmpstr));
	//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
	CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
	dirname((char *)&dirnamestr->data);

	json = json_load_file(fname, 0, &error);
	FAIL_ZERO_BREAK(
	    errctx,
	    json,
	    AKERR_NULLPOINTER,
	    "Error while loading tilemap from %s on line %d: %s-",
	    fname,
	    error.line,
	    error.text
	    );
	CATCH(errctx, akgl_tilemap_load_physics(dest, json));
	CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tileheight", &dest->tileheight));
	CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tilewidth", &dest->tilewidth));
	CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &dest->height));
	CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &dest->width));

	dest->orientation = 0;
	if ( (dest->width * dest->height) >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
	    FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum size");
	}

	CATCH(errctx, akgl_tilemap_load_layers((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));
	CATCH(errctx, akgl_tilemap_load_tilesets((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));

	if ( dest->p_foreground_y && dest->p_vanishing_y ) {
	    // How much bigger is the foreground vs the vanishing point?
	    // if vanishing is height 16, and foreground is height 32, that is a 2x scale difference
	    /* dest->p_scale = ((float)dest->p_foreground_h / (float)dest->p_vanishing_h); */
	    /* SDL_Log("Map perspective scale is (%d/%d) = %f", dest->p_foreground_h, dest->p_vanishing_h, dest->p_scale); */
	    // Sprites are scale N (default 1.0) at the foreground, so how much do we need to
	    // scale them for every pixel above foreground_y before they reach vanishing_y?
	    // If vanishing is at 320 and foreground is at 640, that is a 320 line difference
	    // If our scaling rate is 2x, then our rate is (((N=1.0) / (640 - 320)) / (dest->p_scale = 2)), or
	    // 0.0066% scale per pixel.
	    dest->p_rate = ((dest->p_foreground_scale - dest->p_vanishing_scale) / (dest->p_foreground_y - dest->p_vanishing_y));
	    SDL_Log("Map perspective rate is %f", dest->p_rate);
	}
    } CLEANUP {
	// The map is built entirely out of copies -- layer data, tileset
	// geometry, object names -- so the document is dead the moment the
	// loaders above return, whether they succeeded or not. It is also the
	// largest of the four: a map's JSON is the size of its layer data.
	if ( json != NULL ) {
	    json_decref(json);
	    json = NULL;
	}
	// And the directory the map's relative paths resolve against, which
	// was claimed above and never released -- the other of the two pool
	// strings a map load kept.
	if ( dirnamestr != NULL ) {
	    IGNORE(akgl_heap_release_string(dirnamestr));
	    dirnamestr = NULL;
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, int layeridx)
{
    PREPARE_ERROR(errctx);
    SDL_FRect dest = {.x = 0, .y = 0, .w = 0, .h = 0};;
    SDL_FRect src = {.x = 0, .y = 0, .w = 0, .h = 0};
    int start_x = 0;
    int start_y = 0;
    int end_x = 0;
    int end_y = 0;
    int yidx = 0;
    int xidx = 0;
    int tilesetidx = 0;
    int tilenum = 0;
    int offset = 0;
    /*
     * Render every tile in the map that partially intersects the viewport
     *
     * For an 8x2 tilemap with 16 pixel square tiles like this
     *
     * 01234567
     * 89ABCDEF
     *
     * With a viewport of (x=20, y=8, w=90, y=20), we would render:
     *
     * 123456
     * 9ABCDE
     *
     * 0 and 8 would not be rendered. 1, 9, 6, and E would be partially rendered at their corner.
     * 2,3,4,5 and A,B,C,D would be partially rendered with a slice from their center.
     */
    FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw received NULL pointer to tilemap");
    FAIL_ZERO_RETURN(errctx, viewport, AKERR_NULLPOINTER, "akgl_tilemap_draw received NULL pointer to viewport");

    /* Only try to render the stuff that is partially within the viewport */

    start_x = viewport->x / map->tilewidth;
    start_y = viewport->y / map->tileheight;
    end_x = (viewport->x + viewport->w) / map->tilewidth;
    end_y = (viewport->y + viewport->h) / map->tileheight;
    if ( end_x > map->width ) {
	end_x = map->width;
    }
    if ( end_y > map->height ) {
	end_y = map->height;
    }

    /*SDL_Log("Rendering map into viewport from (%d, %d) to (%d, %d)",
      start_x, start_y, end_x, end_y);*/

    if ( map->layers[layeridx].type == AKGL_TILEMAP_LAYER_TYPE_IMAGE ) {
	dest.x = 0;
	dest.y = 0;
	src.w = map->layers[layeridx].width;
	src.h = map->layers[layeridx].height;
	dest.w = map->layers[layeridx].width;
	dest.h = map->layers[layeridx].height;
	PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
	SUCCEED_RETURN(errctx);
    }

    dest.x = 0;
    dest.y = 0;
    dest.w = map->tilewidth;
    dest.h = map->tileheight;
    for ( yidx = start_y; yidx < end_y; yidx++ ) {
	dest.x = 0;
	for ( xidx = start_x; xidx < end_x; xidx++ ) {
	    if ( yidx == 0 ) {
		offset = xidx;
	    } else {
		offset = xidx + (yidx * (map->width));
	    }
	    tilenum = map->layers[layeridx].data[offset];
	    // FIXME: This is probably not very efficient. Need a better way to look up
	    // tile offsets within the tilesets by their tile ID.
	    for ( tilesetidx = 0; tilesetidx < map->numtilesets ; tilesetidx++ ) {
		if ( map->tilesets[tilesetidx].firstgid <= tilenum &&
		     (map->tilesets[tilesetidx].firstgid + map->tilesets[tilesetidx].tilecount) >= tilenum ) {
		    // Render this tile to the correct screen position
		    // FIXME: These conditionals are probably not very efficient. Need a better way of getting
		    // the intersection of this tile with the viewport and rendering only that portion.
		    if ( xidx == 0 ) {
			src.x += (int)viewport->x % map->tilewidth;
			src.w = map->tilewidth - ((int)viewport->x % map->tilewidth);
		    } else {
			src.x = map->tilesets[tilesetidx].tile_offsets[tilenum - map->tilesets[tilesetidx].firstgid][0];
			src.w = map->tilewidth;
		    }
		    if ( yidx == 0 ) {
			src.y += (int)viewport->y % map->tileheight;
			src.h = map->tileheight - ((int)viewport->y % map->tileheight);
		    } else {
			src.y = map->tilesets[tilesetidx].tile_offsets[tilenum - map->tilesets[tilesetidx].firstgid][1];
			src.h = map->tileheight;
		    }
		    /*SDL_Log("Blitting tile #%d (local tileset id %d from offset %d) from map layer %d map (x=%d,y=%d) tileset %d (x=%f,y=%f,w=%f,h=%f) to (x=%f,y=%f,w=%f,h=%f)",
		      tilenum,
		      (tilenum - map->tilesets[tilesetidx].firstgid),
		      offset,
		      layeridx,
		      xidx,
		      yidx,
		      tilesetidx,
		      src.x,
		      src.y,
		      src.w,
		      src.h,
		      dest.x,
		      dest.y,
		      dest.w,
		      dest.h);*/
		    PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
		}
	    }
	    dest.x += map->tilewidth;
	}
	dest.y += map->tileheight;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
{
    PREPARE_ERROR(errctx);
    SDL_FRect dest;
    SDL_FRect src;
    int tilenum = 0;
    /*
     * Render every tile in a tileset to the default renderer
     * (this is a debugging tool that shows that the recorded tile offsets are correct,
     * by proving that we can reconstruct the original tileset image)
     */

    FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw_tileset received NULL pointer to tilemap");
    FAIL_NONZERO_RETURN(errctx, (tilesetidx >= map->numtilesets), AKERR_OUTOFBOUNDS, "akgl_tilemap_draw_tileset received a tileset index out of bounds");

    for ( tilenum = 0; tilenum < map->tilesets[tilesetidx].tilecount; tilenum++) {
	// Render this tile to the correct screen position
	// FIXME: These conditionals are probably not very efficient. Need a better way of getting
	// the intersection of this tile with the viewport and rendering only that portion.
	src.x = map->tilesets[tilesetidx].tile_offsets[tilenum][0];
	src.y = map->tilesets[tilesetidx].tile_offsets[tilenum][1];
	src.w = map->tilewidth;
	src.h = map->tileheight;
	dest.x = tilenum * map->tilewidth;
	if ( tilenum >= map->tilesets[tilesetidx].columns ) {
	    dest.x = (tilenum % (map->tilesets[tilesetidx].columns)) * map->tilewidth;
	}
	if ( tilenum >= (map->tilesets[tilesetidx].columns) ) {
	    dest.y = (tilenum / (map->tilesets[tilesetidx].columns)) * map->tileheight;
	} else {
	    dest.y = 0;
	}
	dest.w = src.w;
	dest.h = src.h;
	/*SDL_Log("Blitting tile #%d from map tileset %d (x=%f,y=%f,w=%f,h=%f) to (x=%f,y=%f,w=%f,h=%f)",
	  tilenum,
	  tilesetidx,
	  src.x,
	  src.y,
	  src.w,
	  src.h,
	  dest.x,
	  dest.y,
	  dest.w,
	  dest.h);*/
	PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "NULL map");
    FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "NULL actor");

    if ( actor->y <= map->p_vanishing_y ) {
	actor->scale = map->p_vanishing_scale;
    } else if ( actor->y >= map->p_foreground_y ) {
	actor->scale = map->p_foreground_scale;
    } else {
	actor->scale = map->p_foreground_scale - (map->p_rate * (map->p_foreground_y - actor->y));
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akgl_tilemap_release(akgl_Tilemap *dest)
{
    // Release all tileset textures
    // Release all image layer textures
    // Memset to zero
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL map");
    int i = 0;
    // Each pointer is cleared as it goes. Without that a second release
    // destroys textures SDL has already freed, and the second loop used to
    // destroy `tilesets[i]` while testing `layers[i]` -- so every tileset
    // texture was freed twice and no image layer's texture was freed at all.
    for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
	if ( dest->tilesets[i].texture != NULL ) {
	    SDL_DestroyTexture(dest->tilesets[i].texture);
	    dest->tilesets[i].texture = NULL;
	}
    }
    for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
	if ( dest->layers[i].texture != NULL ) {
	    SDL_DestroyTexture(dest->layers[i].texture);
	    dest->layers[i].texture = NULL;
	}
    }
    SUCCEED_RETURN(errctx);
}
