#include <gfx.h>
#include <ps1/gfx_private.h>
#include <psxgpu.h>
#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>

void sprite_free(struct sprite *const src)
{
}

int sprite_clone(const struct sprite *const src, struct sprite *const dst)
{
    *dst = *src;
    return 0;
}

static void sprite_init(struct sprite *const s)
{
    memset(s, 0, sizeof *s);
    s->sznext.f.sz = (sizeof *s - sizeof s->sznext) / sizeof (uint32_t);
    s->common.f.cmd = DRAW_MODE;
    s->cmd = 0x65;
}

struct tim_pos
{
    uint16_t x, y, w, h;
};

static void transfer_init(const struct tim_pos *const p)
{
	while (!(GPU_CONTROL_PORT & (1 << 0x1c)))
        ;

	GPU_CONTROL_PORT = 0x04000000;
	GPU_DATA_PORT = 0x01000000;
	GPU_DATA_PORT = 0xE6000000;
	GPU_DATA_PORT = 0xA0000000;
	GPU_DATA_PORT = (p->y << 16) | p->x;
	GPU_DATA_PORT = (p->h << 16) | p->w;
}

static int transfer(const size_t sz, FILE *const f)
{
    const size_t rem = sz % sizeof (uint32_t);

    if (sz >= sizeof (uint32_t))
    {
        for (size_t i = 0; i < sz / sizeof (uint32_t); i++)
        {
            uint32_t pix;

            if (!fread(&pix, sizeof pix, 1, f))
            {
                fprintf(stderr, "could not read CLUT word %zu/%zu\n",
                    i, sz / sizeof pix);
                return -1;
            }

            GPU_DATA_PORT = pix;
        }
    }

    if (rem)
    {
        uint32_t pix = 0;

        if (!fread(&pix, rem, 1, f))
        {
            fprintf(stderr, "failed reading remaining %zu bytes\n", rem);
            return -1;
        }

        GPU_DATA_PORT = pix;
    }

    return 0;
}

struct header
{
    uint32_t sz;
    struct tim_pos pos;
};

static int upload_clut(struct sprite *const s, FILE *const f)
{
    struct header clut;

    if (!fread(&clut, sizeof clut, 1, f))
    {
        fprintf(stderr, "no CLUT pos data found\n");
        return -1;
    }

    transfer_init(&clut.pos);

    const size_t sz = clut.sz - sizeof clut;

    if (transfer(sz, f))
        return -1;

    s->clutid = get_clutid(clut.pos.x, clut.pos.y);
    return 0;
}

enum bpp
{
    BPP_4 = 0,
    BPP_8 = 1,
    BPP_16 = 2,
    BPP_24 = 4
};

static int upload_img(struct sprite *const s, const enum bpp bpp, FILE *const f)
{
    struct header img;

    if (!fread(&img, sizeof img, 1, f))
    {
        fprintf(stderr, "could not extract image header\n");
        return -1;
    }

    transfer_init(&img.pos);

    const size_t sz = img.sz - sizeof img;

    if (transfer(sz, f))
        return -1;

    enum
    {
        VRAM_X = 1024,
        VRAM_Y = 512,
        TPAGE_WIDTH = 64
    };

    s->common.f.tpagex = img.pos.x / TPAGE_WIDTH;
    s->common.f.tpagey = img.pos.y / (VRAM_Y / 2);
    s->u = img.pos.x % TPAGE_WIDTH;
    s->v = img.pos.y % (VRAM_Y / 2);

    switch (bpp)
    {
        case BPP_4:
            s->w = img.pos.w * 4;
            s->u <<= 2;
            break;

        case BPP_8:
            s->w = img.pos.w * 2;
            s->u <<= 1;
            break;

        case BPP_16:
            s->w = img.pos.w;
            break;

        case BPP_24:
            s->w = img.pos.w + (img.pos.w / 2);
            break;
    }

    s->h = img.pos.h;
    return 0;
}

int sprite_from_fp(struct sprite *const s, FILE *const f)
{
    int ret = -1;

    sprite_init(s);

    struct
    {
        uint32_t version;
        enum bpp bpp :3;
        bool has_clut :1;
        uint32_t :28;
    } h;

    enum {VERSION_ID = 0x10};

    if (!fread(&h, sizeof h, 1, f))
    {
        fprintf(stderr, "TIM header not found\n");
        goto end;
    }
    else if (h.version != VERSION_ID)
    {
        fprintf(stderr, "%s: invalid TIM header %#" PRIx32 "\n", h.version);
        goto end;
    }
    else if (h.bpp == BPP_24)
    {
        fprintf(stderr, "24-bit mode unsupported\n");
        goto end;
    }
    else if ((h.has_clut && upload_clut(s, f))
        || upload_img(s, h.bpp, f))
        goto end;

    s->common.f.bpp = h.bpp ? __builtin_ctz(h.bpp) + 1 : 0;
    ret = 0;

end:
    return ret;
}

int sprite_from_file_ex(const char *const path, struct sprite *const s)
{
    int ret = -1;
    FILE *f = NULL;

    if (!path)
    {
        errno = EINVAL;
        goto end;
    }

    f = fopen(path, "rb");

    if (!f)
    {
        fprintf(stderr, "could not open %s: %s\n", path, strerror(errno));
        goto end;
    }
    else if (sprite_from_fp(s, f))
    {
        fprintf(stderr, "%s: sprite_from_fp failed\n", path);
        goto end;
    }

end:

    if (f)
        fclose(f);

    return ret;
}
