/**
 * @file tilemap.h
 * @brief Loading and drawing Tiled maps: tilesets, layers, embedded actors, per-map physics.
 *
 * The map format is the JSON export from the Tiled editor, so the level design
 * tool is somebody else's and the loader is the only thing that has to know
 * about it. A map is a stack of layers -- tile grids, images, and object groups
 * -- drawn over a set of tilesets, each tileset being one image cut into a grid.
 *
 * Three things beyond ordinary Tiled semantics are worth knowing:
 *
 * - **Object layers can spawn actors.** An object of type `actor` names a
 *   character in its custom properties, and loading the map creates and
 *   registers the actor rather than just recording a rectangle.
 * - **A map can carry its own physics.** A `physics.model` custom property on
 *   the map selects a backend, and `physics.gravity.*` / `physics.drag.*` set
 *   its constants, so a swimming level and a walking level differ by data
 *   rather than by code.
 * - **A map can carry a pseudo-3D perspective.** Two objects named
 *   `p_foreground` and `p_vanishing` in an object layer, each with a `scale`
 *   property, define a band down the screen over which actors are scaled --
 *   which is what makes a character look further away as they walk up the
 *   screen. akgl_tilemap_scale_actor applies it.
 *
 * All paths inside a map -- tileset images, layer images -- are resolved
 * relative to the map file, so a map and its art move together.
 *
 * Note the size of akgl_Tilemap: a layer's tile grid alone is 512x512 ints, and
 * a tileset's offset table is 65536 pairs. It is a megabytes-large object and
 * belongs in static storage, which is where `akgl_default_gamemap` puts it. Do not put
 * one on the stack.
 */

#ifndef _AKGL_TILEMAP_H_
#define _AKGL_TILEMAP_H_

#include <limits.h>
#include <akgl/actor.h>
#include <akgl/staticstring.h>
#include <akgl/physics.h>
#include <jansson.h>

/** @brief Widest map, in tiles. Width times height is what is actually bounded. */
#define AKGL_TILEMAP_MAX_WIDTH                        512
/** @brief Tallest map, in tiles. */
#define AKGL_TILEMAP_MAX_HEIGHT                       512
/** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */
#define AKGL_TILEMAP_MAX_LAYERS                       16
/** @brief Tilesets per map. */
#define AKGL_TILEMAP_MAX_TILESETS                     16
/** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */
#define AKGL_TILEMAP_MAX_TILES_PER_IMAGE              65536
/** @brief Longest tileset name. */
#define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE            512
/** @brief Longest resolved tileset image path. */
#define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE        PATH_MAX
/** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */
#define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE             512
/** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */
#define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER            128

/** @brief Object `type` for a Tiled object whose `type` field is the string "actor". */
#define AKGL_TILEMAP_OBJECT_TYPE_ACTOR                1

/** @brief Layer `type` for a Tiled `tilelayer`: a grid of tile ids. */
#define AKGL_TILEMAP_LAYER_TYPE_TILES                 1
/** @brief Layer `type` for a Tiled `objectgroup`: actors and perspective markers. */
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS               2
/** @brief Layer `type` for a Tiled `imagelayer`: one image drawn at the origin. */
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE                 3

/** @brief One object placed in a Tiled object layer. */
typedef struct akgl_TilemapObject {
    float32_t x;                                        /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
    float32_t y;                                        /**< Position in map pixels. */
    int gid;                                        /**< Global tile id, for a tile object. Not read by the loader. */
    int id;                                         /**< Tiled's object id. Not read by the loader. */
    int height;                                     /**< Height in map pixels. Read into the map's perspective bands, not into this field. */
    int width;                                      /**< Width in map pixels. Not read by the loader. */
    int rotation;                                   /**< Rotation in degrees. Not read by the loader. */
    int type;                                       /**< #AKGL_TILEMAP_OBJECT_TYPE_ACTOR, or 0 for anything else. */
    bool visible;                                   /**< Copied onto the actor. Forced to `false` for perspective markers, which are geometry rather than scenery. */
    akgl_Actor *actorptr;                           /**< The actor this object spawned or attached to, for an actor object. `NULL` otherwise. Borrowed. */
    char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE];   /**< Object name. For an actor object this is the registry key; for a perspective marker it is `p_foreground` or `p_vanishing`. */
} akgl_TilemapObject;

/** @brief One layer of a tilemap: a tile grid, an image, or a group of objects. */
typedef struct akgl_TilemapLayer {
    short type;                                                     /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */
    float32_t opacity;                                                  /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */
    bool visible;                                                   /**< From Tiled. Recorded but not yet consulted at draw time. */
    int height;                                                     /**< Tile layer: height in tiles. Image layer: the texture's height in pixels. */
    int width;                                                      /**< Tile layer: width in tiles. Image layer: the texture's width in pixels. */
    int x;                                                          /**< Layer offset from Tiled. Recorded but not applied at draw time. */
    int y;                                                          /**< Layer offset from Tiled. */
    int id;                                                         /**< Tiled's layer id. Not the same as the index into akgl_Tilemap::layers. */
    SDL_Texture *texture;                                           /**< Image layers only: the whole layer as one texture. `NULL` otherwise. Owned by this struct. */
    int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT];     /**< Tile layers only: global tile ids in row-major order. 0 means an empty cell. */
    akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER]; /**< Object layers only. */
} akgl_TilemapLayer;

/** @brief One tileset: an image cut into a grid, plus the lookup table that finds a tile in it. */
typedef struct akgl_Tileset {
    int columns;                                                /**< Tiles per row in the image. What turns a linear tile id into a row and column. */
    int firstgid;                                               /**< Global id of this tileset's first tile. Subtracting it from a map cell gives the local tile id. */
    char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE]; /**< Resolved absolute path to the image, from the map file's directory. */
    int imageheight;                                            /**< Image height in pixels, as declared by Tiled. */
    int imagewidth;                                             /**< Image width in pixels, as declared. */
    char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE];              /**< Tileset name from Tiled. Diagnostic only; lookups go by `firstgid`. */
    SDL_Texture *texture;                                       /**< The image as one texture. Owned by this struct. */
    // Use this as a lookup table instead of storing tiles
    // in individual textures to blit them from a single
    // texture at runtime
    // FIXME: This is probably not very efficient. For a map
    // with a single tileset it makes sense. For a map with
    // multiple tilesets you may have set A start at firstgid 1
    // and have 1728 tiles. Set B may start at firstgid 1729 and
    // have 1728 more tiles. This means Set B has 1728 empty
    // tile_offsets[] entries before firstgid 1729 because of the
    // way akgl_tilemap_load_tilesets() works. This is really inefficient
    // and should be improved in the future, and will eventually
    // lead to premature exhaustion of AKGL_TILEMAP_MAX_TILES_PER_IMAGE
    // because set D or E may only have 64 tiles but they may be
    // at the upper end of the array bound already because of this.
    int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2];      /**< Local tile id -> {x, y} pixel offset into the image. Computed once at load by akgl_tilemap_compute_tileset_offsets. */
    int tilecount;                                              /**< How many tiles the image holds. */
    int tileheight;                                             /**< Height of one tile in pixels. */
    int tilewidth;                                              /**< Width of one tile in pixels. */
    int spacing;                                                /**< Pixels between adjacent tiles in the image. */
    int margin;                                                 /**< Pixels of border around the whole grid. Recorded but **not** accounted for by the offset computation. */
} akgl_Tileset;

/** @brief Represents a complete tilemap and its optional physics backend. */
typedef struct akgl_Tilemap {
    int tilewidth;                                          /**< Width of one map cell in pixels. Tiles from a tileset with a different tile size are not rescaled. */
    int tileheight;                                         /**< Height of one map cell in pixels. */
    int width;                                              /**< Map width in tiles. */
    int height;                                             /**< Map height in tiles. */
    int numlayers;                                          /**< Layers actually loaded, at most #AKGL_TILEMAP_MAX_LAYERS. */
    int orientation;                                        /**< 0 = orthogonal, 1 = isometric. Always set to 0 by the loader; isometric is not implemented. */
    int numtilesets;                                        /**< Tilesets actually loaded. */
    int p_foreground_y;                                     /**< Y of the `p_foreground` marker: the row at which actors are at full size. 0 disables perspective. */
    int p_vanishing_y;                                      /**< Y of the `p_vanishing` marker: the row at which actors are smallest. 0 disables perspective. */
    int p_foreground_h;                                     /**< Height of the `p_foreground` marker object. Recorded; not used in the current rate calculation. */
    int p_vanishing_h;                                      /**< Height of the `p_vanishing` marker object. Recorded, likewise. */
    float32_t p_foreground_scale;                               /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
    float32_t p_vanishing_scale;                                /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
    float32_t p_scale;                                          /**< Unused. Left from an earlier formulation of the perspective maths. */
    float32_t p_rate;                                           /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */
    akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS];       /**< The tilesets, in the order Tiled listed them. */
    akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS];      /**< The layers, in draw order: index 0 is furthest back. */

    // Different levels may have different physics.
    uint32_t collidablelayers;  /**< Bit `i` set means `layers[i]` is solid geometry for collision, from that layer's `collidable` custom property in Tiled. 0 means nothing on this map collides, which is what a map authored before collision existed gets. */
    bool use_own_physics;                                   /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `akgl_physics` instead of the global backend. */
    akgl_PhysicsBackend physics;                            /**< This map's own backend, valid only when `use_own_physics` is set. */
} akgl_Tilemap;

/**
 * @brief Load a Tiled JSON map: physics, geometry, layers, tilesets, and any actors it spawns.
 *
 * Zeroes @p dest, then reads the map's own physics properties, its tile
 * dimensions, its size in tiles, its layers, and its tilesets -- in that order,
 * because layers reference tilesets by global id and objects reference
 * characters by name. Finally, if the map carried both perspective markers, it
 * works out the per-pixel scaling rate between them.
 *
 * Loading a map has side effects beyond @p dest: tileset and layer images are
 * uploaded as textures, and every `actor` object in an object layer is created
 * in the actor pool and published in #AKGL_REGISTRY_ACTOR. So the renderer, the
 * pools, the registries, and the characters the map names all have to be in
 * place first.
 *
 * @param fname Path to the map JSON. Required. Its directory becomes the root
 *              that every tileset and layer image inside is resolved against.
 * @param dest  The tilemap to fill in. Required. Zeroed first, so a map already
 *              loaded into it has its textures leaked rather than destroyed --
 *              call akgl_tilemap_release first if you are reusing one. Do not
 *              put one of these on the stack; see the note on this file.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p fname or @p dest is `NULL`; if the map file
 *         cannot be opened or does not parse, with jansson's line and text; or
 *         if a tileset image cannot be loaded.
 * @throws AKERR_OUTOFBOUNDS If the map is larger than
 *         #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT tiles, if it has
 *         more than #AKGL_TILEMAP_MAX_LAYERS layers, or if a layer's own
 *         declared size exceeds the same bound.
 * @throws AKERR_KEY If a required key is absent, if an `actor` object has an
 *         empty name, or if the map names a physics model that does not exist.
 * @throws AKERR_TYPE If a key is present with the wrong JSON type.
 * @throws ENOENT If @p fname or a path referenced inside it does not exist.
 * @throws AKGL_ERR_SDL If a layer image cannot be loaded.
 * @throws AKGL_ERR_HEAP If the string or actor pool is exhausted.
 *
 * @note Nothing is released on a failure part-way through: textures already
 *       uploaded and actors already created stay. Treat a failed load as
 *       needing akgl_tilemap_release and a fresh actor registry.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
/**
 * @brief Draw one layer of a map, clipped to a viewport.
 *
 * Only the tiles that intersect @p viewport are drawn, and the tiles at its
 * edges are drawn partially, so scrolling is smooth rather than snapping to the
 * tile grid. An image layer ignores the viewport entirely and is drawn once at
 * the origin.
 *
 * Drawing goes through the global `akgl_renderer`, not through a backend passed in.
 *
 * @param map      The map to draw from. Required.
 * @param viewport The rectangle of the map, in map pixels, that is on screen.
 *                 Required. Usually the global `akgl_camera`.
 * @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not**
 *                 bounds-checked: an index past the end reads a neighbouring
 *                 layer, or past the array.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p map or @p viewport is `NULL`.
 * @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first
 *         failure abandons the rest of the layer.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, int layeridx);
/**
 * @brief Draw a whole tileset to the screen, tile by tile, from its offset table.
 *
 * A debugging tool, not a rendering path. It reconstructs the original tileset
 * image out of the offsets computed at load time, so if the picture that appears
 * matches the source image the offset table is right -- and if tiles are shifted
 * or duplicated, it is not. The `charviewer` utility uses it.
 *
 * @param map        The map holding the tileset. Required.
 * @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p map is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A
 *         negative index is not rejected.
 * @throws AKERR_* Whatever the renderer's `draw_texture` raises.
 *
 * @note Tiles are laid out using the *map's* tile size rather than the
 *       tileset's, so a tileset whose tiles are a different size from the map's
 *       is reconstructed at the wrong pitch.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx);

/*
 * These functions are part of the internal API and should not be called by the user.
 * They are only exposed here for unit testing.
 */

/**
 * @brief Find one entry in a Tiled `properties` array by name, checking its declared type.
 *
 * Tiled does not store custom properties as JSON object members. It stores them
 * as an array of `{"name": ..., "type": ..., "value": ...}` objects, so getting
 * one means a linear scan and a type-string comparison rather than a lookup.
 * That is what this does; the `akgl_get_json_properties_*` wrappers put a typed
 * face on it.
 *
 * @param obj  The Tiled object -- map, layer, or object -- whose `properties`
 *             array to search. Required.
 * @param key  The property name to find. Required.
 * @param type The type string Tiled wrote, e.g. `"string"`, `"int"`, `"float"`.
 *             A property found under the right name but the wrong type is an
 *             error, not a miss. Not `NULL`-checked.
 * @param dest Receives a borrowed pointer to the whole property object -- the
 *             one with `name`, `type`, and `value` -- not to its value. Not
 *             `NULL`-checked.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
 * @throws AKERR_KEY If @p obj has no `properties` array, or if no entry in it
 *         has that name.
 * @throws AKERR_TYPE If `properties` is not an array, an entry is malformed, or
 *         the named property's declared type is not @p type. That last message
 *         reports both the expected and the actual type.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
/**
 * @brief Read a Tiled custom property declared as `string`.
 * @param obj  The Tiled object whose `properties` array to search. Required.
 * @param key  The property name. Required.
 * @param dest Receives the value in a *newly claimed* pool string. Required. Note
 *             that this always claims -- unlike akgl_get_json_string_value it
 *             does not reuse a string already in `*dest`, so passing one leaks
 *             it. The caller releases the result with akgl_heap_release_string.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
 * @throws AKERR_KEY If the property is absent, or @p obj has no properties.
 * @throws AKERR_TYPE If the property is not declared `string`.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
/**
 * @brief Read a Tiled custom property declared as `int`.
 * @param obj  The Tiled object whose `properties` array to search. Required.
 * @param key  The property name. Required.
 * @param dest Receives the value. Not written on any failure path.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
 * @throws AKERR_KEY If the property is absent, or @p obj has no properties.
 * @throws AKERR_TYPE If the property is not declared `int`, or its `value` is
 *         not an integer.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);

/**
 * @brief Read a boolean Tiled custom property.
 *
 * Tiled writes these with a declared type of `"bool"`, and the accessor matches
 * on that type -- a property written as an `int` named the same thing raises
 * `AKERR_TYPE` rather than being coerced, because a map that says `1` where it
 * means `true` is a map that will say something else next time.
 *
 * @param obj  The object carrying a `properties` array. Required.
 * @param key  Property name. Required.
 * @param dest Receives the value. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_KEY If no property of that name exists. **Absent is not an
 *         error to the loader** -- a layer with no `collidable` property is not
 *         collidable, which is the common case.
 * @throws AKERR_TYPE If the property exists but was not written as a bool.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_boolean(json_t *obj, char *key, bool *dest);
/**
 * @brief Build a tileset's local-id-to-pixel-offset table.
 *
 * Run once per tileset at load time so drawing a tile is an array lookup rather
 * than a division. Walks the grid left to right and top to bottom, stepping by
 * tile size plus `spacing`.
 *
 * @param dest       The map holding the tileset. Required in practice, though
 *                   not checked -- it is dereferenced immediately.
 * @param tilesetidx Which tileset, 0 to `numtilesets - 1`. Not bounds-checked.
 * @return `NULL`. There is no failure path: it is arithmetic into an already
 *         allocated table.
 *
 * @note Two known limits. `margin` is not accounted for, so a tileset image with
 *       a border produces offsets shifted by it. And the table is indexed by
 *       local id from 0, while akgl_tilemap_draw indexes it by
 *       `tilenum - firstgid`, so the entries a second tileset needs sit at the
 *       front of its own table -- see the FIXME on akgl_Tileset::tile_offsets
 *       for why that wastes space.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
/**
 * @brief Load one object layer: spawn its actors and record its perspective markers.
 *
 * Two kinds of object are understood. `actor` objects name a registered actor,
 * or create one -- claiming it from the pool, initializing it, and binding the
 * character named in its `character` property -- and place it at the object's
 * coordinates on this layer. `perspective` objects named `p_foreground` or
 * `p_vanishing` set the map's scaling band and are themselves invisible.
 * Anything else is recorded and otherwise ignored.
 *
 * An actor named by two objects is not duplicated: the second takes another
 * reference on the first.
 *
 * @param dest    The map to load into. Required.
 * @param root    The layer's JSON object -- not the map root. Required.
 * @param layerid Index of the layer being loaded, which becomes each spawned
 *                actor's `layer`. Not bounds-checked.
 * @param dirname Directory to resolve paths against. Accepted for signature
 *                consistency with the other layer loaders; nothing here uses it.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
 * @throws AKERR_KEY If the layer has no `objects` array, if a required key is
 *         absent, or if an `actor` object has an empty name.
 * @throws AKERR_TYPE If a key is present with the wrong JSON type.
 * @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
 * @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
 * @throws AKERR_* Whatever akgl_actor_initialize or akgl_actor_set_character
 *         raises -- notably AKERR_KEY if the named character is not registered.
 *
 * @warning The object count is not checked against
 *          #AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, so a layer with more than 128
 *          objects writes past the array.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
 * @brief Load one tile layer's grid of global tile ids.
 *
 * @param dest    The map to load into. Required.
 * @param root    The layer's JSON object -- not the map root. Required.
 * @param layerid Index of the layer being loaded. Not bounds-checked.
 * @param dirname Directory to resolve paths against. Required, though a tile
 *                layer references no files and nothing here uses it.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If the layer's declared `width` times `height`
 *         reaches #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT, or if the
 *         `data` array is shorter than that product.
 * @throws AKERR_KEY If `height`, `width`, or `data` is absent.
 * @throws AKERR_TYPE If one of them has the wrong JSON type, or a cell is not an
 *         integer.
 *
 * @note The declared `width` and `height` are trusted over the actual length of
 *       `data`: a layer declaring more cells than it lists fails on the read
 *       past the end rather than on a length check.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
 * @brief Load every layer of a map, dispatching on each layer's declared type.
 *
 * Reads the common fields -- id, opacity, visibility, offset -- then hands the
 * layer to the loader for its kind: `objectgroup`, `tilelayer`, or `imagelayer`.
 * A layer of any other type keeps its common fields and is otherwise skipped,
 * with `type` left at 0.
 *
 * @param dest    The map to load into. Required. `numlayers` is set from the
 *                array's length before any bound is checked, so it may briefly
 *                exceed #AKGL_TILEMAP_MAX_LAYERS on the failure path.
 * @param root    The map's root JSON object. Required.
 * @param dirname Directory to resolve layer image paths against. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If the map has more than #AKGL_TILEMAP_MAX_LAYERS
 *         layers.
 * @throws AKERR_KEY If the map has no `layers` array, or a layer is missing a
 *         common field.
 * @throws AKERR_TYPE If a field has the wrong JSON type.
 * @throws AKERR_* Whatever the per-kind loader raises.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
/**
 * @brief Load one tileset's metadata and upload its image.
 *
 * @param tileset The tileset's JSON object. Required in practice, though not
 *                checked -- it is passed straight to the accessors, which
 *                report it.
 * @param dest    The map to load into. Required, unchecked, dereferenced at once.
 * @param tsidx   Which slot to fill, 0 to #AKGL_TILEMAP_MAX_TILESETS - 1. Not
 *                bounds-checked.
 * @param dirname Directory to resolve the tileset's `image` path against --
 *                the map file's own directory. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If a required key is missing its object, or if the
 *         tileset image fails to load. The image message carries
 *         `SDL_GetError()`.
 * @throws AKERR_KEY If one of `columns`, `firstgid`, `imageheight`,
 *         `imagewidth`, `margin`, `spacing`, `tilecount`, `tileheight`,
 *         `tilewidth`, `name`, or `image` is absent.
 * @throws AKERR_TYPE If one of them has the wrong JSON type.
 * @throws AKERR_OUTOFBOUNDS If the resolved image path is too long for a pooled
 *         string.
 * @throws ENOENT If the image path does not exist.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 *
 * @note This does not go through the spritesheet registry, so a tileset image
 *       shared between two maps is loaded twice.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
/**
 * @brief Load every tileset of a map and compute each one's offset table.
 *
 * @param dest    The map to load into. Required. `numtilesets` is reset to 0 and
 *                incremented as each tileset succeeds, so it always reflects
 *                what actually loaded.
 * @param root    The map's root JSON object. Required.
 * @param dirname Directory to resolve tileset image paths against. Required in
 *                practice; not checked here.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
 * @throws AKERR_KEY If the map has no `tilesets` array.
 * @throws AKERR_TYPE If `tilesets` is not an array of objects.
 * @throws AKERR_* Whatever akgl_tilemap_load_tilesets_each raises.
 *
 * @warning The tileset count is not checked against
 *          #AKGL_TILEMAP_MAX_TILESETS, so a map with more than 16 tilesets
 *          writes past the array.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
/**
 * @brief Destroy the textures a map owns.
 *
 * Call before reusing a tilemap struct for a different map; akgl_tilemap_load
 * zeroes rather than releases, so without this the previous map's textures are
 * leaked.
 *
 * @param dest The map to release. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p dest is `NULL`.
 *
 * @warning Known defect: the layer loop destroys `tilesets[i].texture` rather
 *          than `layers[i].texture`, so tileset textures are destroyed twice and
 *          image-layer textures never. Nothing is set to `NULL` either, so a
 *          second call is a use-after-free. It also does not release the actors
 *          the map's object layers created. TODO.md, "Defects"
 *          item 2.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
/**
 * @brief Scale an actor for its distance up the screen, using the map's perspective band.
 *
 * The pseudo-3D trick: an actor at or below `p_foreground_y` is drawn at
 * `p_foreground_scale`, one at or above `p_vanishing_y` at `p_vanishing_scale`,
 * and one in between is interpolated linearly. A map with no perspective markers
 * has both scales at 1.0 and a rate of 0, so this is a no-op rather than a
 * special case.
 *
 * Only the actor's `scale` is written; nothing is drawn. akgl_game_update calls
 * it before each actor's update when #AKGL_ITERATOR_OP_TILEMAPSCALE is set.
 *
 * @param map   The map supplying the perspective band. Required.
 * @param actor The actor to scale. Required. Its `y` is read and its `scale`
 *              written.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p map or @p actor is `NULL`.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);

/**
 * @brief Read a Tiled custom property declared as `number`.
 *
 * Tiled writes `float` for a floating-point property, so this matches nothing
 * Tiled produces today -- akgl_get_json_properties_float is the one the loader
 * uses. Kept because a hand-written or older map may still say `number`.
 *
 * @param obj  The Tiled object whose `properties` array to search. Required.
 * @param key  The property name. Required.
 * @param dest Receives the value as a `float`. Not written on any failure path.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
 * @throws AKERR_KEY If the property is absent, or @p obj has no properties.
 * @throws AKERR_TYPE If the property is not declared `number`, or its `value` is
 *         not numeric.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_number(json_t *obj, char *key, float32_t *dest);
/**
 * @brief Read a Tiled custom property declared as `float`.
 *
 * The spelling Tiled actually writes for a floating-point property. This is how
 * the `scale` on a perspective marker is read.
 *
 * @param obj  The Tiled object whose `properties` array to search. Required.
 * @param key  The property name. Required.
 * @param dest Receives the value. Not written on any failure path.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
 * @throws AKERR_KEY If the property is absent, or @p obj has no properties.
 * @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
 *         not numeric.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_float(json_t *obj, char *key, float32_t *dest);
/**
 * @brief Read a Tiled custom property declared as `float`, at full precision.
 *
 * Same declared type as akgl_get_json_properties_float, but writing a `double`.
 * The map's physics constants are `double`, which is the reason both exist.
 *
 * @param obj  The Tiled object whose `properties` array to search. Required.
 * @param key  The property name. Required.
 * @param dest Receives the value. Not written on any failure path.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
 * @throws AKERR_KEY If the property is absent, or @p obj has no properties.
 *         akgl_tilemap_load_physics passes this status to
 *         akgl_get_json_with_default, which turns it into a default of 0.0.
 * @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
 *         not numeric.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_double(json_t *obj, char *key, float64_t *dest);
/**
 * @brief Turn one Tiled object into a live actor, creating it if it is not already registered.
 *
 * The object's name is the actor's registry key, which is what lets the same
 * actor be placed by more than one object: the first placement creates it and
 * binds the character named in the object's `character` property, and every
 * later one just takes another reference. Either way the object's position,
 * visibility, and layer are copied onto the actor and the object keeps a
 * pointer to it.
 *
 * Not declared in tilemap.h -- reachable only through
 * akgl_tilemap_load_layer_objects.
 *
 * @param curobj         The tilemap object, with its `name`, `x`, `y`, and
 *                       `visible` already read from JSON. Required, unchecked,
 *                       dereferenced at once.
 * @param layerdatavalue The object's JSON, for the custom properties --
 *                       `character` and `state` -- that are not part of Tiled's
 *                       own object fields. Required.
 * @param layerid        The layer this object sits on, copied onto the actor.
 * @param dirname        Directory to resolve paths against. Accepted for
 *                       signature consistency; nothing here uses it.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_KEY If the object's name is empty -- an actor has to be
 *         addressable -- if the `character` or `state` property is absent, or if
 *         the named character is not in #AKGL_REGISTRY_CHARACTER.
 * @throws AKERR_TYPE If `character` is not declared `string` or `state` not
 *         declared `int`.
 * @throws AKERR_NULLPOINTER If akgl_actor_initialize refuses the name.
 * @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
 *
 * @note An existing actor's `character` and position are overwritten by each
 *       later placement, so two objects naming one actor leave it wherever the
 *       last one put it rather than producing two.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname);
/**
 * @brief Load one image layer: upload its image as a single texture.
 *
 * An image layer is one picture rather than a grid, which is what a parallax
 * backdrop wants. The layer's `width` and `height` are taken from the loaded
 * texture rather than from the JSON, so they are always the true pixel size.
 *
 * Not declared in tilemap.h -- reachable only through akgl_tilemap_load_layers.
 *
 * @param dest    The map to load into. Required.
 * @param root    The layer's JSON object. Required.
 * @param layerid Which layer slot to fill. Not bounds-checked.
 * @param dirname Directory to resolve the layer's `image` path against.
 *                Required. Joined with a `/` rather than run through
 *                akgl_path_relative, so unlike a tileset image this path is not
 *                canonicalized and an absolute one will not work.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
 * @throws AKERR_KEY If the layer has no `image`.
 * @throws AKERR_TYPE If `image` is not a string.
 * @throws AKGL_ERR_SDL If the image cannot be loaded. The message carries
 *         `SDL_GetError()`.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 * @throws AKERR_OUTOFBOUNDS If `dirname` and the image name do not fit in
 *         #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE when joined, naming both
 *         lengths. This used to truncate silently, so an over-long path failed
 *         as a missing file rather than as the length error it is.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
 * @brief Give the map its own physics backend, if it asked for one.
 *
 * A map with a `physics.model` custom property gets its own backend, configured
 * from `physics.gravity.x`/`.y`/`.z` and `physics.drag.x`/`.y`/`.z`, and
 * `use_own_physics` is set so a caller knows to simulate through it. Each
 * constant defaults to 0.0 when absent, so a map can name a model and override
 * only what it cares about.
 *
 * Absence is the normal case at every level: a map with no properties at all, or
 * with properties but no `physics.model`, uses the game's global backend and
 * this returns success. That is why the AKERR_KEY handlers here are not error
 * paths -- they are the "map did not ask" path.
 *
 * Not declared in tilemap.h -- reachable only through akgl_tilemap_load.
 *
 * @param dest The map to configure. Required in practice; not checked here,
 *             since akgl_tilemap_load has already done so.
 * @param root The map's root JSON object. Required, likewise.
 * @return `NULL` on success -- including when the map declared no physics at
 *         all -- otherwise an error context owned by the caller.
 * @throws AKERR_KEY If `physics.model` names a backend that does not exist.
 * @throws AKERR_TYPE If `physics.model` is not declared `string`, or one of the
 *         six constants is not declared `float`.
 * @throws AKERR_NULLPOINTER If akgl_physics_factory refuses its arguments.
 * @throws AKGL_ERR_HEAP If the string pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root);

#endif //_AKGL_TILEMAP_H_
