#include "map.h"

#include "../core/logger.h"
#include "../core/memory.h"
#include "../math/vec2i.h"

#include <cute_tiled.h>
#include <raymath.h>
#include <string.h>

BzTileMap BZ_TILEMAP_INVALID = {.isValid = false};

void bzTileLayerSkipRender(BzTileMap *map, BzTileLayer *layer) {
    BZ_UNUSED(map);
    BZ_UNUSED(layer);
}
void bzTileObjectGroupSkipRender(BzTileMap *map, BzTileObjectGroup *objectGroup) {
    BZ_UNUSED(map);
    BZ_UNUSED(objectGroup);
}

BzTileLayerRenderFunc BZ_TILE_LAYER_SKIP_RENDER = bzTileLayerSkipRender;
BzTileObjectGroupRenderFunc BZ_TILE_OBJECTS_SKIP_RENDER = bzTileObjectGroupSkipRender;

bool bzTileLayerClear(BzTileMap *map, BzTileLayer *layer) {
    BZ_UNUSED(map);
    BZ_UNUSED(layer);
    return true;
}
bool bzTileObjectsClear(BzTileMap *map, BzTileObjectGroup *objectGroup) {
    BZ_UNUSED(map);
    BZ_UNUSED(objectGroup);
    return true;
}

BzTileLayerFunc BZ_TILE_LAYER_CLEAR = bzTileLayerClear;
BzTileObjectsFunc BZ_TILE_OBJECTS_CLEAR = bzTileObjectsClear;

BzTile bzTileLayerGetTile(BzTileLayer *layer, i32 x, i32 y) {
    return layer->data[layer->width * y + x];
}
void bzTileLayerSetTile(BzTileLayer *layer, BzTile tile, i32 x, i32 y, i32 w, i32 h) {
    for (i32 yIdx = y; yIdx < y + h; yIdx++) {
        for (i32 xIdx = x; xIdx < x + w; xIdx++) {
            layer->data[yIdx * layer->width + xIdx] = tile;
        }
    }
}
BzTileset *bzTileLayerGetTileset(BzTileMap *map, BzTileLayer *layer) {
    i32 idx = layer->tilesetIdx;
    if (idx < 0 || idx >= map->tilesetCount)
        return NULL;
    return &map->tilesets[idx];
}
BzTileset *bzTileObjectGroupGetTileset(BzTileMap *map, BzTileObjectGroup *objectGroup) {
    i32 idx = objectGroup->tilesetIdx;
    if (idx < 0 || idx >= map->tilesetCount)
        return NULL;
    return &map->tilesets[idx];
}

static void handleTileLayer(BzTileLayer *layer, cute_tiled_layer_t *cuteLayer) {
    layer->id = cuteLayer->id;
    layer->dataCount = cuteLayer->data_count;
    layer->data = NULL;
    if (layer->dataCount > 0) {
        layer->data = bzAlloc(layer->dataCount * sizeof(*layer->data));
        layer->minData = layer->maxData = (BzTile) cuteLayer->data[0];
        for (i32 i = 0; i < layer->dataCount; i++) {
            layer->data[i] = (BzTile) cuteLayer->data[i];
            if (layer->data[i] < layer->minData)
                layer->minData = layer->data[i];
            else if (layer->data[i] > layer->maxData)
                layer->maxData = layer->data[i];
        }
    }

    layer->width = cuteLayer->width;
    layer->height = cuteLayer->height;

    layer->offsetX = cuteLayer->offsetx;
    layer->offsetY = cuteLayer->offsety;
    layer->opacity = cuteLayer->opacity;

}

// tileset.c
BzTileShape bzCuteObjectToTileShape(cute_tiled_object_t *object);

static void handleTileObjectLayer(BzTileObjectGroup *layer, cute_tiled_layer_t *cuteLayer,
                                  BzStringHashFunc hashFunc) {
    // Count objects
    layer->objectCount = 0;
    cute_tiled_object_t *object = cuteLayer->objects;
    while (object) {
        layer->objectCount++;
        object = object->next;
    }

    layer->objects = bzAlloc(layer->objectCount * sizeof(*layer->objects));
    object = cuteLayer->objects;
    for (i32 i = 0; i < layer->objectCount; i++) {
        layer->objects[i].id = hashFunc(object->name.ptr);

        layer->objects[i].gid = object->gid;
        if (layer->minGID > object->gid) layer->minGID = object->gid;
        else if (layer->maxGID < object->gid) layer->maxGID = object->gid;

        layer->objects[i].shape = bzCuteObjectToTileShape(object);
        object = object->next;
    }

}

static void updateCollisionMap(BzTileMap *map, i32 startX, i32 startY, i32 endX, i32 endY) {
    BZ_ASSERT(map->collisionMap);
    BZ_ASSERT(startX >= 0 && endX <= map->width &&
              startY >= 0 && endY <= map->height);

    for (i32 y = startY; y < endY; y++) {
        for (i32 x = startX; x < endX; x++) {
            map->collisionMap[y * map->width + x] = false;
        }
    }

    // Top-most layer takes priority
    for (i32 i = map->layerCount - 1; i >= 0; i--) {
        BzTileLayer *layer = map->layers + i;
        if (!layer->desc.applyColliders) continue;
        if (!layer->data) continue;
        if (layer->tilesetIdx == -1) continue;
        BzTileset *tileset = map->tilesets + layer->tilesetIdx;
        for (i32 y = startY; y < endY; y++) {
            for (i32 x = startX; x < endX; x++) {
                i32 idx = y * map->width + x;
                if (map->collisionMap[idx]) continue;

                i32 tile = bzTileLayerGetTile(layer, x, y);
                BzTileShape tilesetShape = bzTilesetGetTileCollider(tileset, tile);
                if (tilesetShape.type == BZ_TILE_SHAPE_NONE ||
                    tilesetShape.type == BZ_TILE_SHAPE_POINT)
                    continue;

                map->collisionMap[idx] = true;
            }
        }
    }
}

static void createColliders(BzTileMap *map) {
    map->collisionMap = bzAlloc(map->width * map->height * sizeof(*map->collisionMap));
    updateCollisionMap(map, 0, 0, map->width, map->height);
}

BzTileMap bzTileMapCreate(const BzTileMapDesc *desc) {
    BzTileMap map = {.backgroundColor=BLACK};
    // Auto detect tileset count.
    for (i32 i = 0; i < BZ_MAP_MAX_TILESETS; i++) {
        if (!desc->tilesets[i].isValid)
            break;
        map.tilesetCount++;
    }
    for (i32 i = 0; i < map.tilesetCount; i++) {
        map.tilesets[i] = desc->tilesets[i];
    }

    cute_tiled_map_t *cuteMap = cute_tiled_load_map_from_file(desc->path, NULL);

    map.backgroundColor = GetColor(cuteMap->backgroundcolor);

    map.width = cuteMap->width;
    map.height = cuteMap->height;
    map.tileWidth = cuteMap->tilewidth;
    map.tileHeight = cuteMap->tileheight;

    cute_tiled_layer_t *cuteLayer = cuteMap->layers;
    while (cuteLayer) {
        BZ_ASSERT(map.layerCount < BZ_MAP_MAX_LAYERS);
        BZ_ASSERT(map.objectGroupCount < BZ_MAP_MAX_LAYERS);

        // Find slot
        i32 slot = -1;
        for (i32 i = 0; i < BZ_MAP_MAX_LAYERS; i++) {
            const BzTileLayerDesc *layerDesc = desc->layers + i;
            const BzTileObjectsDesc *objectsDesc = desc->objectGroups + i;
            if ((layerDesc->name && strcmp(layerDesc->name, cuteLayer->name.ptr) == 0) ||
                (objectsDesc->name && strcmp(objectsDesc->name, cuteLayer->name.ptr) == 0)) {
                slot = i;
                break;
            } else if (!layerDesc->name && !objectsDesc->name) {
                break;
            }
        }
        if (slot == -1) {
            bzLogWarning("Dangling layer: %s", cuteLayer->name.ptr);
            cuteLayer = cuteLayer->next;
            continue;
        }

        if (cuteLayer->data) {
            BzTileLayer *layer = map.layers + slot;
            layer->desc = desc->layers[slot];
            handleTileLayer(layer, cuteLayer);
            layer->hasOwnership = true;
            map.layerCount++;
        } else {
            BzTileObjectGroup *objectGroup = map.objectGroups + slot;
            objectGroup->desc = desc->objectGroups[slot];
            if (!objectGroup->desc.hashFunc)
                objectGroup->desc.hashFunc = bzStringDefaultHash;
            handleTileObjectLayer(objectGroup, cuteLayer, objectGroup->desc.hashFunc);

            map.objectGroupCount++;
        }

        cuteLayer = cuteLayer->next;
    }

    cute_tiled_tileset_t *cuteTileset = cuteMap->tilesets;
    int tilesetCount = 0;
    for (; cuteTileset; cuteTileset = cuteTileset->next) tilesetCount++;
    BZ_ASSERT(tilesetCount == map.tilesetCount);

    cuteTileset = cuteMap->tilesets;
    for (i32 i = 0; i < map.tilesetCount; i++) {
        BzTileset *tileset = map.tilesets + i;
        tileset->startID = cuteTileset->firstgid;
        cuteTileset = cuteTileset->next;
    }

    // Assign tilesets to layers
    for (i32 i = 0; i < BZ_MAP_MAX_LAYERS; i++) {
        BzTileLayer *layer = map.layers + i;
        layer->tilesetIdx = -1;
        BzTileObjectGroup *objects = map.objectGroups + i;
        objects->tilesetIdx = -1;

        bool hasLayer = i < map.layerCount;
        bool hasObjects = i < map.objectGroupCount;

        for (i32 j = map.tilesetCount - 1; j  >= 0; j--) {
            BzTileset *tileset = map.tilesets + j;

            if (hasLayer && tileset->startID >= layer->minData &&
                tileset->startID <= layer->maxData) {
                layer->tilesetIdx = j;
                hasLayer = false;
            }
            if (hasObjects && tileset->startID >= objects->minGID &&
                tileset->startID <= objects->maxGID) {
                objects->tilesetIdx = j;
                hasObjects = false;
            }
            if (!hasLayer && !hasObjects) break;
        }
    }
    cute_tiled_free_map(cuteMap);

    if (desc->generateCollisionMap)
        createColliders(&map);

    map.isValid = true;
    return map;
}

void bzTileMapDestroy(BzTileMap *map) {
    for (i32 i = 0; i < map->layerCount; i++) {
        BzTileLayer *layer = map->layers + i;
        if (layer->hasOwnership && layer->data) {
            bzFree(layer->data);
            layer->data = NULL;
            layer->dataCount = 0;
        }
    }
    for (i32 i = 0; i < map->objectGroupCount; i++) {
        BzTileObjectGroup *objectLayer = map->objectGroups + i;
        if (objectLayer->hasOwnership && objectLayer->objects) {
            bzFree(objectLayer->objects);
            objectLayer->objects = NULL;
            objectLayer->objectCount = 0;
        }
    }

    bzFree(map->collisionMap);

    *map = BZ_TILEMAP_INVALID;
}


void bzTileMapPosToTile(BzTileMap *map, Vector2 pos, BzTile *x, BzTile *y) {
    if (x) *x = (BzTile) (pos.x / (f32) map->tileWidth);
    if (y) *y = (BzTile) (pos.y / (f32) map->tileHeight);
}

void bzTileMapOverrideLayer(BzTileMap *map, i32 slotID, BzTileLayerFunc func) {
    BZ_ASSERT(slotID >= 0 && slotID < map->layerCount);
    BzTileLayer *layer = map->layers + slotID;
    i32 dataCount = layer->dataCount;
    BzTile *data = layer->data;
    if (func(map, layer)) {
        if (!layer->hasOwnership) return;
        bzFree(layer->data);
        if (layer->data == data) {
            layer->data = NULL;
            layer->dataCount = 0;
        }
        layer->hasOwnership = false;
    } else {
        BZ_ASSERT(data == layer->data && dataCount == layer->dataCount);
    }
}
void bzTileMapOverrideObjectGroup(BzTileMap *map, i32 slotID, BzTileObjectsFunc func) {
    BZ_ASSERT(slotID >= 0 && slotID < map->objectGroupCount);
    BzTileObjectGroup *objectGroup = map->objectGroups + slotID;
    BzTileObject *objects = objectGroup->objects;
    i32 objectCount = objectGroup->objectCount;
    if (func(map, objectGroup)) {
        bzFree(objectGroup->objects);
        if (objectGroup->objects == objects) {
            objectGroup->objects = NULL;
            objectGroup->objectCount = 0;
        }
        objectGroup->hasOwnership = false;
    } else {
        BZ_ASSERT(objects == objectGroup->objects && objectCount == objectGroup->objectCount);
    }
}


static void drawLayer(BzTileLayer *layer, BzTileset *tileset) {
    if (!tileset) return;
    if (layer->minData == layer->maxData) return;
    Vector2 drawPos = {layer->offsetX, layer->offsetY};

    for (i32 y = 0; y < layer->height; y++) {
        for (i32 x = 0; x < layer->width; x++) {
            BzTile tile = bzTileLayerGetTile(layer, x, y);
            if (tile - tileset->startID != -1) {
                Rectangle rec = bzTilesetGetTileRegion(tileset, tile);
                DrawTextureRec(tileset->tiles, rec, drawPos, WHITE);
            }
            drawPos.x += (float) tileset->tileWidth;
        }
        drawPos.x = layer->offsetX;
        drawPos.y += (float) tileset->tileHeight;
    }

}

static void drawObjectLayer(BzTileObjectGroup *objectLayer, BzTileset *tileset) {
    Color color = ORANGE;
    for (int i = 0; i < objectLayer->objectCount; i++) {
        BzTileObject object = objectLayer->objects[i];
        BzTileShape shape = object.shape;
        switch (shape.type) {
            case BZ_TILE_SHAPE_NONE:
                break;
            case BZ_TILE_SHAPE_POINT:
                DrawCircle(shape.x, shape.y, 2.0f, color);
                break;
            case BZ_TILE_SHAPE_RECT:
                if (object.gid) {
                    Rectangle rec = bzTilesetGetTileRegion(tileset, object.gid);
                    Rectangle dest = {shape.x, shape.y - shape.sizeY, shape.sizeX, shape.sizeY};
                    DrawTexturePro(tileset->tiles, rec, dest, (Vector2) {0.0f, 0.0f}, 0.0f, WHITE);
                } else {
                    DrawRectangle(shape.x, shape.y, shape.sizeX, shape.sizeY, color);
                }
                break;
            case BZ_TILE_SHAPE_ELLIPSE:
                DrawEllipse(shape.x, shape.y, shape.sizeX, shape.sizeY, color);
                break;
        }
    }
}

BzTileLayer *bzTileMapGetLayer(BzTileMap *map, i32 slotID) {
    BZ_ASSERT(slotID >= 0 && slotID < map->layerCount);
    return &map->layers[slotID];
}
BzTileObjectGroup *bzTileMapGetObjects(BzTileMap *map, i32 slotID) {
    BZ_ASSERT(slotID >= 0 && slotID < map->objectGroupCount);
    return &map->objectGroups[slotID];
}
static f32 abs(f32 x) {
    return x > 0 ? x : -x;
}

f32 bzTileMapRayCast(BzTileMap *map, Vector2 from, Vector2 to, f32 maxDst, Vector2 *outIntersection) {
    // Digital Differential Analyzer (DDA)
    Vector2 scale = {1.0f / map->tileWidth, 1.0f / map->tileHeight};
    from = Vector2Multiply(from, scale);
    to = Vector2Multiply(to, scale);

    Vector2 dir = Vector2Subtract(to, from);
    dir = Vector2Normalize(dir);

    //Vector2 stepSize = {
    //        sqrtf(1 + (dir.y / dir.x) * (dir.y / dir.x)),
    //        sqrtf(1 + (dir.x / dir.y) * (dir.x / dir.y))
    //};
    Vector2 stepSize = {abs(1.0f / dir.x), abs(1.0f / dir.y)};

    i32 cellX = (i32) from.x;
    i32 cellY = (i32) from.y;
    if (bzTileMapHasCollision(map, cellX, cellY)) {
        return 0.0f;
    }
    Vector2 rayLength = Vector2Zero();
    i32 stepX, stepY;

    if (dir.x < 0) {
        stepX = -1;
        rayLength.x = (from.x - (f32) (cellX)) * stepSize.x;
    } else {
        stepX = 1;
        rayLength.x = ((f32) cellX + 1.0f - from.x) * stepSize.x;
    }

    if (dir.y < 0) {
        stepY = -1;
        rayLength.y = (from.y - (f32) cellY) * stepSize.y;
    } else {
        stepY = 1;
        rayLength.y = ((f32) cellY + 1 - from.y) * stepSize.y;
    }
    f32 distance = 0.0f;
    while (distance < maxDst) {
        if (rayLength.x < rayLength.y) {
            cellX += stepX;
            distance = rayLength.x * map->tileWidth;
            rayLength.x += stepSize.x;
        } else {
            cellY += stepY;
            distance = rayLength.y * map->tileHeight;
            rayLength.y += stepSize.y;
        }
        if (cellX >= 0 && cellX < map->width && cellY >= 0 && cellY < map->height &&
            map->collisionMap[cellY * map->width + cellX]) {
            break;
        }
    }

    if (outIntersection) {
        Vector2 intersection = from;
        intersection = Vector2Add(intersection, Vector2Scale(dir, distance));
        intersection.x *= map->tileWidth;
        intersection.y *= map->tileHeight;
        *outIntersection = intersection;
    }

    return distance;
}
bool bzTileMapCanRayCastLine(BzTileMap *map, Vector2 from, Vector2 to) {
    f32 dst = Vector2Distance(from, to);
    f32 rayDst = bzTileMapRayCast(map, from, to, dst, NULL);
    return rayDst >= dst;
}

void bzTileMapDraw(BzTileMap *map) {
    for (i32 i = 0; i < map->layerCount; i++) {
        BzTileLayer *layer = map->layers + i;
        if (!layer->data) continue;
        BzTileset *tileset = NULL;
        if (layer->tilesetIdx != -1) {
            tileset = map->tilesets + layer->tilesetIdx;
        }
        drawLayer(map->layers + i, tileset);
    }

    for (i32 i = 0; i < map->objectGroupCount; i++) {
        BzTileObjectGroup *objectLayer = map->objectGroups + i;
        BzTileset *tileset = NULL;
        if (objectLayer->tilesetIdx != -1) {
            tileset = map->tilesets + objectLayer->tilesetIdx;
        }
        if (!objectLayer->objects) continue;
        drawObjectLayer(objectLayer, tileset);
    }
}

void bzTileMapDrawCollisions(BzTileMap *map) {
    if (!map->collisionMap) return;
    Color color = RED;
    color.a = 150;
    i32 sizeX = map->tileWidth;
    i32 sizeY = map->tileHeight;
    for (i32 y = 0; y < map->height; y++) {
        for (i32 x = 0; x < map->width; x++) {
            i32 idx = y * map->width + x;
            i32 posX = x * sizeX;
            i32 posY = y * sizeY;
            if (map->collisionMap[idx])
            DrawRectangleLines(posX, posY, sizeX, sizeY, color);
        }
    }
}

bool bzTileMapHasCollision(BzTileMap *map, i32 x, i32 y) {
    if (!map->collisionMap) return false;
    i32 idx = y * map->width + x;
    return map->collisionMap[idx];
}

void bzTileMapUpdateCollisions(BzTileMap *map, i32 x, i32 y, i32 sizeX, i32 sizeY) {
    if (!map->collisionMap) return;
    updateCollisionMap(map, x, y, sizeX, sizeY);
}
