/*
 * src/tmx.c
 * https://gitlab.com/bztsrc/assetpack
 *
 * Copyright (C) 2024 bzt, MIT license
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL ANY
 * DEVELOPER OR DISTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * @brief Handle Tiled TMX files
 */

#include "main.h"

/**
 * Decode a TMX string into a map
 */
apck_map_t *tmx_decode(uint8_t *buf, uint64_t size)
{
    apck_map_t *map = NULL;
    apck_map_layer_t *layer;
    uint32_t *idx;
    char *str = (char*)buf, *end = (char*)buf + size, *ptr, *fn, *ext;
    int i, n, w, h, len, parX, parY, offX, offY, movX, movY, disptype, encoding;

    if(!buf || !size || (memcmp(buf, "<?xml", 5) && memcmp(buf, "<map", 4)))
        return NULL;

    /* locate header and allocate map */
    while(str < end - 4 && memcmp(str, "<map", 4)) str++;
    if(str >= end - 4 || !(map = apck_map_new(APCK_ORTHOGONAL, 1, 1, 1, 1, 0, NULL))) {
        fprintf(stderr, "unable to allocate map\n");
        return NULL;
    }
    /* parse header */
    for(str += 4; str < end && *str != '>'; str++) {
        if(!memcmp(str, "orthogonal", 10)) map->type = APCK_ORTHOGONAL; else
        if(!memcmp(str, "isometric", 9)) map->type = APCK_ISOMETRIC; else
        if(!memcmp(str, "staggeraxis=\"y\"", 15)) map->type = APCK_VERTHEX; else
        if(!memcmp(str, "staggeraxis=\"x\"", 15)) map->type = APCK_HORIZHEX; else
        if(!memcmp(str, " width=", 7))  { for(str += 7; str < end && str[-1] != '\"'; str++){} map->w = atoi(str); } else
        if(!memcmp(str, " height=", 8))  { for(str += 8; str < end && str[-1] != '\"'; str++){} map->h = atoi(str); } else
        if(!memcmp(str, " tilewidth=", 11))  { for(str += 11; str < end && str[-1] != '\"'; str++){} map->tilew = atoi(str); } else
        if(!memcmp(str, " tileheight=", 12))  { for(str += 12; str < end && str[-1] != '\"'; str++){} map->tileh = atoi(str); }
    }

    /* parse layers */
    for(; str < end; str++) {
        if(!memcmp(str, "<layer ", 7) || !memcmp(str, "<imagelayer ", 12)) {
            disptype = offX = offY = movX = movY = 0; w = map->w; h = map->h; parX = parY = 100;
            for(str += 7; str < end && *str != '>'; str++) {
                if(!memcmp(str, " width=", 7))  { for(str += 7; str < end && str[-1] != '\"'; str++){} w = atoi(str); } else
                if(!memcmp(str, " height=", 8))  { for(str += 8; str < end && str[-1] != '\"'; str++){} h = atoi(str); } else
                if(!memcmp(str, " offsetx=", 9))  { for(str += 9; str < end && str[-1] != '\"'; str++){} offX = atoi(str); } else
                if(!memcmp(str, " offsety=", 9))  { for(str += 9; str < end && str[-1] != '\"'; str++){} offY = atoi(str); } else
                if(!memcmp(str, " parallaxx=", 11))  { for(str += 11; str < end && str[-1] != '\"'; str++){} parX = (int)(atof(str) * 100); } else
                if(!memcmp(str, " parallaxy=", 11))  { for(str += 11; str < end && str[-1] != '\"'; str++){} parY = (int)(atof(str) * 100); } else
                if(!memcmp(str, " movementx=", 11))  { for(str += 11; str < end && str[-1] != '\"'; str++){} movX = atoi(str); } else
                if(!memcmp(str, " movementy=", 11))  { for(str += 11; str < end && str[-1] != '\"'; str++){} movY = atoi(str); } else
                if(!memcmp(str, " repeatx", 8))  { disptype |= 1; } else
                if(!memcmp(str, " repeaty", 8))  { disptype |= 2; }
            }
            for(; str < end && *str != '<'; str++);
            if(!memcmp(str, "<data", 5)) {
                /* if it's a tile layer */
                for(encoding = 0, str += 5; str < end && *str != '>'; str++) {
                    if(!memcmp(str, "csv", 3)) encoding = 1; else
                    if(!memcmp(str, "base64", 6)) encoding = 2; else
                    if(!memcmp(str, "zlib", 4)) encoding = 3; else
                    if(!memcmp(str, "gzip", 4)) encoding = 3;
                }
                if(encoding && str < end && *str == '>') {
                    n = w * h * sizeof(uint32_t);
                    if((idx = (uint32_t*)malloc(n))) {
                        /* read in (possibly cropped) layer data */
                        memset(idx, 0, n);
                        for(ptr = ++str; str < end && memcmp(str, "</data", 6); str++);
                        for(;ptr < str && (*ptr == ' ' || *ptr == '\t' || *ptr == '\r' || *ptr == '\n'); ptr++);
                        len = (int)(str - ptr);
                        if(encoding > 1) len = base64_decode(ptr, (uint8_t*)ptr, len);
                        switch(encoding) {
                            case 1: /* csv */
                                for(i = 0, n = w * h; i < n && ptr < str; ptr++)
                                    if(*ptr >= '0' && *ptr <= '9') {
                                        idx[i++] = (uint32_t)atol(ptr);
                                        while(ptr + 1 < str && ptr[1] >= '0' && ptr[1] <= '9') ptr++;
                                    }
                            break;
                            case 2: /* binary */
                                memcpy(idx, ptr, len > n ? n : len);
                            break;
                            case 3: /* deflate (with zlib or gzip header) */
                                if(ptr[0] == 0x1f) { ptr += 8; len -= 8; ptr[0] = 0x78; ptr[1] = 0x01; }
                                if(!stbi_zlib_decode_buffer((char*)idx, n, (char const *)ptr, len)) {
                                    free(idx); idx = NULL;
                                    if(!quiet) fprintf(stderr, "uncompression error on layer data\n");
                                }
                            break;
                        }
                        /* if we have the data, we can add the layer */
                        if(idx) {
                            if((layer = apck_map_layer(map, APCK_MAP, 0, NULL))) {
                                layer->disptype = disptype;
                                layer->parX = parX;
                                layer->parY = parY;
                                layer->offX = offX;
                                layer->offY = offY;
                                layer->movX = movX;
                                layer->movY = movY;
                                /* copy layer data. Layer's width and hight might be different than map's */
                                n = w;
                                if(n > map->w) n = map->w;
                                if(h > map->h) h = map->h;
                                for(i = 0; i < h; i++)
                                    memcpy((uint32_t*)layer->data + i * map->w, idx + i * w, n * 4);
                            } else
                            if(!quiet) fprintf(stderr, "unable to add map layer\n");
                            free(idx);
                        }
                    } else
                        fprintf(stderr, "unable to allocate memory for layer (w %d h %d)\n", w, h);
                } else
                if(!quiet) fprintf(stderr, "unknown layer data encoding\n");
            } else
            if(!memcmp(str, "<image", 6)) {
                /* if it's an image layer */
                for(str += 6; str < end && *str != '>'; str++)
                    if(!memcmp(str, "source=", 7)) {
                        for(str += 6; str < end && str[-1] != '\"'; str++);
                        for(ptr = str; str < end && *str != '\"'; str++);
                        if(str < end && *str == '\"') {
                            *str = 0;
                            if((fn = strrchr(ptr, '/'))) fn++; else
                            if((fn = strrchr(ptr, '\\'))) fn++; else fn = ptr;
                            if((ext = strrchr(fn, '.'))) *ext = 0;
                            if((layer = apck_map_layer(map, !memcmp(ptr, "models", 6) ? APCK_MODEL : APCK_IMAGE, 0, fn))) {
                                layer->disptype = disptype;
                                layer->parX = parX;
                                layer->parY = parY;
                                layer->offX = offX;
                                layer->offY = offY;
                            } else
                            if(!quiet) fprintf(stderr, "unable to add map layer\n");
                        } else
                        if(!quiet) fprintf(stderr, "missing source tag on image\n");
                    }
            } else
            if(!quiet) fprintf(stderr, "unknown layer type\n");
        }
    }
    return map;
}

/**
 * Encode a map into a TMX string
 */
uint8_t *tmx_encode(apck_map_t *map, uint64_t *size)
{
    uint8_t *ret;
    uint32_t *idx;
    char *str;
    int i, j, n;

    if(!map || !size || map->w < 1 || map->w < 1) return NULL;

    /* overestimate the required buffer length */
    if(!(ret = (uint8_t*)malloc(256 + map->l * (256 + map->h * (2 + map->w * 12))))) {
        fprintf(stderr, "unable to allocate memory\n");
        return NULL;
    }

    /* write out header */
    str = (char*)ret + sprintf((char*)ret, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<map version=\"1.5\" orientation=\"");
    switch(map->type) {
        case APCK_ISOMETRIC: str += sprintf(str, "isometric"); break;
        case APCK_VERTHEX: str += sprintf(str, "hexagonal\" staggeraxis=\"y"); break;
        case APCK_HORIZHEX: str += sprintf(str, "hexagonal\" staggeraxis=\"x"); break;
        default: str += sprintf(str, "orthogonal"); break;
    }
    str += sprintf(str, "\" width=\"%u\" height=\"%u\" tilewidth=\"%u\" tileheight=\"%u\" infinite=\"0\" nextlayerid=\"%u\">\r\n",
        map->w, map->h, map->tilew, map->tileh, map->l + 1);

    /* write out layers */
    for(j = 0; j < map->l && map->layers; j++)
        if(map->layers[j].data)
            switch(map->layers[j].type) {
                /* if it's a tile layer */
                case APCK_MAP:
                    str += sprintf(str, " <layer id=\"%u\" width=\"%u\" height=\"%u\"",
                        j + 1, map->w, map->h);
                    if(map->layers[j].offX) str += sprintf(str, " offsetx=\"%d\"", map->layers[j].offX);
                    if(map->layers[j].offY) str += sprintf(str, " offsety=\"%d\"", map->layers[j].offY);
                    if(map->layers[j].parX != 100) str += sprintf(str, " parallaxx=\"%d.%d\"", map->layers[j].parX / 100, map->layers[j].parX % 100);
                    if(map->layers[j].parY != 100) str += sprintf(str, " parallaxy=\"%d.%d\"", map->layers[j].parY / 100, map->layers[j].parY % 100);
                    if(map->layers[j].movX) str += sprintf(str, " movementx=\"%d\"", map->layers[j].movX);
                    if(map->layers[j].movY) str += sprintf(str, " movementy=\"%d\"", map->layers[j].movY);
                    str += sprintf(str, ">\r\n  <data encoding=\"csv\">\r\n");
                    for(idx = (uint32_t*)map->layers[j].data, n = map->w * map->h, i = 0; i < n; i++)
                        str += sprintf(str, "%u%s%s", idx[i], i + 1 < n ? "," : "", i && (i % map->w) == map->w - 1 ? "\r\n" : "");
                    str += sprintf(str, "  </data>\r\n </layer>\r\n");
                break;
                /* if it's an image layer */
                default:
                    str += sprintf(str, " <imagelayer id=\"%u\" offsetx=\"%d\" offsety=\"%d\" parallaxx=\"%d.%d\" parallaxy=\"%d.%d\"",
                        j + 1, map->layers[j].offX, map->layers[j].offY, map->layers[j].parX / 100, map->layers[j].parX % 100,
                        map->layers[j].parY / 100, map->layers[j].parY % 100);
                    if(map->layers[j].disptype & 1) str += sprintf(str, " repeatx=\"1\"");
                    if(map->layers[j].disptype & 2) str += sprintf(str, " repeaty=\"1\"");
                    str += sprintf(str, ">\r\n  <image source=\"%s%s.%s\"/>\r\n </imagelayer>\r\n",
                        map->layers[j].type == APCK_MODEL ? "models/" : (map->layers[j].type == APCK_IMAGE ? "images/" : ""),
                        (char*)map->layers[j].data, map->layers[j].type == APCK_MODEL ? "m3d" : "png");
                break;
            }
    str += sprintf(str, "</map>\r\n");
    *str = 0;
    *size = (uintptr_t)str - (uintptr_t)ret;
    /* free memory because we have overestimated for the allocation */
    ret = (uint8_t*)realloc(ret, (*size) + 1);
    return ret;
}
