#include "meta.h"
#include "../coding/coding.h"
#include "../util/cri_utf.h"


typedef enum { HCA, CWAV, ADX } cpk_type_t;

static void load_cpk_name(STREAMFILE* sf, STREAMFILE* sf_acb, VGMSTREAM* vgmstream, int waveid);

/* CPK - CRI container as audio bank [Metal Gear Solid: Snake Eater 3D (3DS), Street Fighter X Tekken (X360), Ace Combat Infinity (PS3)] */
VGMSTREAM* init_vgmstream_cpk(STREAMFILE* sf) {
    return init_vgmstream_cpk_memory(sf, NULL);
}

VGMSTREAM* init_vgmstream_cpk_memory(STREAMFILE* sf, STREAMFILE* sf_acb) {
    VGMSTREAM* vgmstream = NULL;
    STREAMFILE* temp_sf = NULL;
    off_t subfile_offset = 0;
    size_t subfile_size = 0;
    utf_context* utf = NULL;
    int total_subsongs, target_subsong = sf->stream_index;
    int subfile_id = 0;
    cpk_type_t type;
    const char* extension = NULL;
    uint32_t* sizes = NULL;


    /* checks */
    if (!is_id32be(0x00,sf, "CPK "))
        goto fail;
    if (!check_extensions(sf, "awb"))
        goto fail;

    if (!is_id32be(0x10,sf, "@UTF"))
        goto fail;
    /* 04: 0xFF? */
    /* 08: 0x02A0? */
    /* 0c: null? */

    /* CPK .cpk is CRI's generic file container, but here we only support CPK .awb used as
     * early audio bank, that like standard AFS2 .awb comes with .acb */
    {
        int rows, i;
        const char* name;
        const char* Tvers;
        uint32_t table_offset = 0, offset;
        uint32_t Files = 0, FilesL = 0, FilesH = 0;
        uint64_t ContentOffset = 0, ItocOffset = 0;
        uint16_t Align = 0;
        uint32_t DataL_offset = 0, DataL_size = 0, DataH_offset = 0, DataH_size = 0;

        /* base header */
        table_offset = 0x10;
        utf = utf_open(sf, table_offset, &rows, &name);
        if (!utf || strcmp(name, "CpkHeader") != 0 || rows != 1)
            goto fail;

        if (!utf_query_string(utf, 0, "Tvers", &Tvers) ||
            !utf_query_u32(utf, 0, "Files", &Files) ||
            !utf_query_u64(utf, 0, "ContentOffset", &ContentOffset) || /* absolute */
            !utf_query_u64(utf, 0, "ItocOffset", &ItocOffset) || /* Toc seems used for regular files */
            !utf_query_u16(utf, 0, "Align", &Align))
            goto fail;

        utf_close(utf);
        utf = NULL;

        if (strncmp(Tvers, "awb", 3) != 0) /* starts with "awb" + ".(version)" (SFvTK, MGS3D) or " for (version)" (ACI) */
            goto fail;
        if (Files <= 0)
            goto fail;


        /* Itoc header (regular .CPK tend to use Toc or Etoc header) */
        table_offset = 0x10 + ItocOffset;
        utf = utf_open(sf, table_offset, &rows, &name);
        if (!utf) goto fail;

        if (rows != 1 || strcmp(name, "CpkItocInfo") != 0)
            goto fail;

        if (!utf_query_u32(utf, 0, "FilesL", &FilesL) || 
            !utf_query_u32(utf, 0, "FilesH", &FilesH) ||
            !utf_query_data(utf, 0, "DataL", &DataL_offset, &DataL_size) || /* absolute */
            !utf_query_data(utf, 0, "DataH", &DataH_offset, &DataH_size))   /* absolute */
            goto fail;

        utf_close(utf);
        utf = NULL;


        /* For maximum annoyance there are 2 tables (small+big files) that only list sizes,
         * and files can be mixed (small small big small big).
         * Must pre-read all entries to find actual offset plus subsongs number. */
        if (FilesL + FilesH != Files)
            goto fail;

        total_subsongs = Files;
        if (target_subsong == 0) target_subsong = 1;
        if (target_subsong > total_subsongs || total_subsongs <= 0) goto fail;

        sizes = calloc(Files, sizeof(uint32_t));
        if (!sizes) goto fail;

        /* DataL header */
        table_offset = DataL_offset;
        utf = utf_open(sf, table_offset, &rows, &name);
        if (!utf || strcmp(name, "CpkItocL") != 0 || rows != FilesL)
            goto fail;

        for (i = 0; i < rows; i++) {
            uint16_t ID = 0;
            uint16_t FileSize, ExtractSize;

            if (!utf_query_u16(utf, i, "ID", &ID) ||
                !utf_query_u16(utf, i, "FileSize", &FileSize) ||
                !utf_query_u16(utf, i, "ExtractSize", &ExtractSize))
                goto fail;

            if (ID >= Files || FileSize != ExtractSize || sizes[ID])
                goto fail;

            sizes[ID] = FileSize;
        }

        utf_close(utf);
        utf = NULL;

        /* DataR header */
        table_offset = DataH_offset;
        utf = utf_open(sf, table_offset, &rows, &name);
        if (!utf || strcmp(name, "CpkItocH") != 0 || rows != FilesH)
            goto fail;

        for (i = 0; i < rows; i++) {
            uint16_t ID = 0;
            uint32_t FileSize, ExtractSize;

            if (!utf_query_u16(utf, i, "ID", &ID) ||
                !utf_query_u32(utf, i, "FileSize", &FileSize) ||
                !utf_query_u32(utf, i, "ExtractSize", &ExtractSize))
                goto fail;

            if (ID >= Files || FileSize != ExtractSize || sizes[ID])
                goto fail;

            sizes[ID] = FileSize;
        }

        utf_close(utf);
        utf = NULL;


        /* find actual offset */
        offset = ContentOffset;
        for (i = 0; i < Files; i++) {
            uint32_t size = sizes[i];
            if (i + 1 == target_subsong) {
                subfile_id = i;
                subfile_offset = offset;
                subfile_size = size;
                break;
            }

            offset += size;
            if (Align && (offset % Align))
                offset += Align - (offset % Align);
        }
        
        free(sizes);
        sizes = NULL;
    }

    if (!subfile_offset)
        goto fail;

    //;VGM_LOG("CPK: subfile offset=%lx + %x, id=%i\n", subfile_offset, subfile_size, subfile_id);


    if ((read_u32be(subfile_offset,sf) & 0x7f7f7f7f) == get_id32be("HCA\0")) {
        type = HCA;
        extension = "hca";
    }
    else if (is_id32be(subfile_offset,sf, "CWAV")) {
        type = CWAV;
        extension = "bcwav";
    }
    else if (read_u16be(subfile_offset, sf) == 0x8000) {
        type = ADX;
        extension = "adx";
    }
    else {
        goto fail;
    }

    temp_sf = setup_subfile_streamfile(sf, subfile_offset, subfile_size, extension);
    if (!temp_sf) goto fail;

    switch(type) {
        case HCA:
            vgmstream = init_vgmstream_hca(temp_sf);
            if (!vgmstream) goto fail;
            break;
        case CWAV: /* Metal Gear Solid: Snake Eater 3D (3DS) */
            vgmstream = init_vgmstream_bcwav(temp_sf);
            if (!vgmstream) goto fail;
            break;
        case ADX: /* Sonic Generations (3DS) */
            vgmstream = init_vgmstream_adx(temp_sf);
            if (!vgmstream) goto fail;
            break;
        default:
            goto fail;
    }

    vgmstream->num_streams = total_subsongs;

    /* try to load cue names */
    load_cpk_name(sf, sf_acb, vgmstream, subfile_id);

    close_streamfile(temp_sf);
    return vgmstream;

fail:
    free(sizes);
    utf_close(utf);
    close_streamfile(temp_sf);
    close_vgmstream(vgmstream);
    return NULL;
}

static void load_cpk_name(STREAMFILE* sf, STREAMFILE* sf_acb, VGMSTREAM* vgmstream, int waveid) {
    int is_memory = (sf_acb != NULL);
    int port = -1; /* cpk has no port numbers */

    /* .acb is passed when loading memory .awb inside .acb */
    if (!is_memory) {
        /* try parsing TXTM if present */
        sf_acb = read_filemap_file(sf, 0);

        /* try (name).awb + (name).awb */
        if (!sf_acb)
            sf_acb = open_streamfile_by_ext(sf, "acb");

        /* (name)_streamfiles.awb + (name).acb also exist */

        if (!sf_acb)
            return;

        /* companion .acb probably loaded */
        load_acb_wave_name(sf_acb, vgmstream, waveid, port, is_memory);

        close_streamfile(sf_acb);
    }
    else {
        load_acb_wave_name(sf_acb, vgmstream, waveid, port, is_memory);
    }
}
