#include "meta.h"
#include "../coding/coding.h"
#include "zsnd_streamfile.h"


/* ZSND - Z-Axis/Vicarious Visions games [X-Men Legends II (multi), Marvel Ultimate Alliance (multi)] */
VGMSTREAM * init_vgmstream_zsnd(STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;
    STREAMFILE *temp_streamFile = NULL;
    off_t start_offset, name_offset;
    size_t stream_size, name_size;
    int loop_flag, channel_count, sample_rate, layers, layers2 = 0;
    uint32_t codec;
    int total_subsongs, target_subsong = streamFile->stream_index;
    int32_t (*read_32bit)(off_t,STREAMFILE*) = NULL;
    int16_t (*read_16bit)(off_t,STREAMFILE*) = NULL;


    /* checks */
    /* .zss/zsm: standard
     * .ens/enm: same for PS2
     * .zsd: normal or compact [BMX XXX (Xbox), Aggresive Inline (Xbox)] */
    if (!check_extensions(streamFile, "zss,zsm,ens,enm,zsd"))
        goto fail;
    if (read_32bitBE(0x00,streamFile) != 0x5A534E44) /* "ZSND" */
        goto fail;
    /* probably zss=stream, zsm=memory; no diffs other than size */

    codec = read_32bitBE(0x04,streamFile);
    /* 0x08: file size, but slightly bigger (+0x01~04) in some platforms */
    /* 0x0c: header end/first stream start (unneeded as all offsets are absolute) */

    if (codec == 0x47435542) { /* "GCUB" */
        read_32bit = read_32bitBE;
        read_16bit = read_16bitBE;
    }
    else {
        read_32bit = read_32bitLE;
        read_16bit = read_16bitLE;
    }


    /* parse header tables */
    {
        off_t header2_offset, header3_offset;
        int   table2_entries, table3_entries;
        off_t table2_body, table3_body;
        int is_v1, i;


        /* multiple config tables:
         *  0x00: entries
         *  0x04: table head offset
         *  0x08: table body offset
         *
         * table heads are 0x08 * entries:
         *  0x00 = id? (crc-like, varies between tables but consistent between platforms)
         *  0x04 = entry? (number, also in table2_body?)
         *
         * main tables:
         *  table1: stream cues? (entry=0x18)
         *  table2: stream heads (optional, rarely not all stream bodies may have heads)
         *  table3: stream body
         *  table4: unknown, very rare, some kind of seek table with numbers going up? (Aggresive Inline: speech01.zsd)
         *  table5: unknown, very rare, (X-Men Legends II: boss4_m.zsm)
         *  table6/7: not seen
         *
         * table1 may have more entries than table2/3, and sometimes isn't set
         */

        /* V1 has no table heads, rare [Aggresive Inline (Xbox)]
         * no apparent flag but we can test if table heads offsets appear */
        is_v1 = read_32bit(0x14,streamFile) <= read_32bit(0x1c,streamFile) &&
                read_32bit(0x1c,streamFile) <= read_32bit(0x24,streamFile) &&
                read_32bit(0x24,streamFile) <= read_32bit(0x2c,streamFile) &&
                read_32bit(0x2c,streamFile) <= read_32bit(0x34,streamFile) &&
                read_32bit(0x34,streamFile) <= read_32bit(0x3c,streamFile) &&
                read_32bit(0x3c,streamFile) <= read_32bit(0x44,streamFile);

        if (!is_v1) {
            table2_entries = read_32bit(0x1c,streamFile);
            table2_body    = read_32bit(0x24,streamFile);

            table3_entries = read_32bit(0x28,streamFile);
            table3_body    = read_32bit(0x30,streamFile);
        }
        else {
            table2_entries = read_32bit(0x18,streamFile);
            table2_body    = read_32bit(0x1C,streamFile);

            table3_entries = read_32bit(0x20,streamFile);
            table3_body    = read_32bit(0x24,streamFile);
        }

        total_subsongs = table3_entries;

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

        switch (codec) {
            case 0x50432020: /* "PC  " */
                if (table2_entries == 0) goto fail;

                header2_offset = table2_body + 0x18*(target_subsong-1);
                layers       = read_16bit(header2_offset + 0x02,streamFile);
                sample_rate  = read_32bit(header2_offset + 0x04,streamFile);

                header3_offset = table3_body + 0x4c*(target_subsong-1);
                start_offset = read_32bit(header3_offset + 0x00,streamFile);
                stream_size  = read_32bit(header3_offset + 0x04,streamFile);
                name_offset  = header3_offset + 0x0c;
                name_size    = 0x40;
                break;

            case 0x58424F58: { /* "XBOX" */
                size_t entry2_size = is_v1 || check_extensions(streamFile, "zsd") ? 0x14 : 0x1c;

                /* BMX has unordered stream headers, and not every stream has a header */
                header2_offset = 0;
                for (i = 0; i < table2_entries; i++) {
                    int16_t id = read_16bit(table2_body + entry2_size*i + 0x00,streamFile);

                    if (id >= 0 && id + 1 != target_subsong) /* can be -1 == deleted entry */
                        continue;
                    header2_offset = table2_body + entry2_size*i;
                    break;
                }

                if (header2_offset == 0) {
                    if (table2_entries > 0) {
                        /* seems usable for sfx, meh */
                        header2_offset = table2_body + entry2_size*0;
                        layers       = read_16bit(header2_offset + 0x02,streamFile);
                        sample_rate  = read_32bit(header2_offset + 0x04,streamFile);
                    }
                    else {
                        layers       = 0;
                        sample_rate  = 0;
                    }
                }
                else {
                    layers       = read_16bit(header2_offset + 0x02,streamFile);
                    sample_rate  = read_32bit(header2_offset + 0x04,streamFile);
                    if (entry2_size > 0x18) {
                        layers2 = read_32bit(header2_offset + 0x18,streamFile);
                    }
                }

                header3_offset = table3_body + 0x54*(target_subsong-1);
                start_offset = read_32bit(header3_offset + 0x00,streamFile);
                stream_size  = read_32bit(header3_offset + 0x04,streamFile);
                /* 0x08: flags? related to looping? (not channels) */
              //loop_end     = read_32bit(header3_offset + 0x10,streamFile);
                name_offset  = header3_offset + 0x14;
                name_size    = 0x40;

                /* early games sometimes don't seem to have info or headers, not sure how to detect better
                 * ex. Aggresive Inline speech (1ch) vs music (2ch), or BMX cutscenes (2ch) */
                if (sample_rate == 0) {
                    int is_music = 0;
                    if (is_v1) {
                        char filename[PATH_LIMIT];

                        /* stream length isn't enough */
                        get_streamfile_filename(streamFile, filename, sizeof(filename));
                        is_music = strcmp(filename, "music.zsd") == 0;
                    }
                    else {
                        is_music = stream_size > 0x20000;
                    }

                    if (is_music) {
                        layers       = 0x02;
                        sample_rate  = 44100;
                    }
                    else {
                        layers       = 0x00;
                        sample_rate  = is_v1 ? 16000 : 22050; /* some BMX need 16000 but can't detect? */
                    }
                }

                break;
            }

            case 0x50533220: /* "PS2 " (also for PSP) */
                if (table2_entries == 0) goto fail;

                header2_offset = table2_body + 0x10*(target_subsong-1);
                sample_rate  = read_16bit(header2_offset + 0x02,streamFile);
                layers       = read_16bit(header2_offset + 0x04,streamFile);

                header3_offset = table3_body + 0x08*(target_subsong-1);
                start_offset = read_32bit(header3_offset + 0x00,streamFile);
                stream_size  = read_32bit(header3_offset + 0x04,streamFile);
                name_offset  = 0;
                name_size    = 0;

                /* pitch value, with 0x1000=44100 (voices vary quite a bit, ex. X-Men Legends 2) */
                sample_rate = round10(sample_rate * 44100.0 / 4096.0);
                /* there may be some rounding for lower values, ex 0x45A = 11993.99 ~= 12000, though not all: 
                 * 0x1000 = 44100, 0x0800 = 22050, 0x0687 ~= 18000, 0x05ce ~= 16000, 0x045a ~= 12000, 0x0400 = 11025 */
                break;

            case 0x47435542: /* "GCUB" (also for Wii) */
                header2_offset = table2_body + 0x18*(target_subsong-1);
                layers        = read_16bit(header2_offset + 0x02,streamFile);
                sample_rate   = read_32bit(header2_offset + 0x04,streamFile);

                header3_offset = table3_body + 0x0c*(target_subsong-1);
                start_offset  = read_32bit(header3_offset + 0x00,streamFile);
                stream_size   = read_32bit(header3_offset + 0x04,streamFile);
                /* 0x08: "DSP " for some reason */
                name_offset   = 0;
                name_size     = 0;
                break;

            default:
                goto fail;
        }

        /* maybe flags? */
        switch (layers) {
            case 0x00: channel_count = 1; break;
            case 0x01: channel_count = 1; break; /* set when looping? */
            case 0x02: channel_count = 2; break;
            case 0x22: channel_count = 4; break;
            default:
                VGM_LOG("ZSND: unknown layers\n");
                goto fail;
        }

        if (layers2) {
            channel_count = channel_count * layers2;
        }

        loop_flag = 0;
    }


    /* build the VGMSTREAM */
    vgmstream = allocate_vgmstream(channel_count, loop_flag);
    if (!vgmstream) goto fail;

    vgmstream->meta_type = meta_ZSND;
    vgmstream->sample_rate = sample_rate;

    switch (codec) {
        case 0x50432020: /* "PC  " */
            vgmstream->coding_type = coding_IMA;
            vgmstream->layout_type = layout_none;
            //todo interleaved stereo (needs to adapt decoder)
            //vgmstream->layout_type = layout_interleave; /* interleaved stereo for >2ch*/
            //vgmstream->interleave_block_size = 0x2000 * 2 / channel_count;

            vgmstream->num_samples = ima_bytes_to_samples(stream_size, channel_count);
            break;

        case 0x58424F58: /* "XBOX" */
            vgmstream->coding_type = coding_XBOX_IMA;
            vgmstream->layout_type = layout_interleave; /* interleaved stereo for >2ch*/
            vgmstream->interleave_block_size = 0x9000 * 2 / channel_count;

            vgmstream->num_samples = xbox_ima_bytes_to_samples(stream_size, channel_count);

            /* very rarely entries refer to external .wma, but redoing the logic to handle only real
             * streams handle is a pain, so signal this case with an empty file [Aggresive Inline (Xbox)] */
            if (vgmstream->num_samples == 0) {
                vgmstream->num_samples = 48;
            }
            break;

        case 0x50533220: /* "PS2 " (also for PSP) */
            vgmstream->coding_type = coding_PSX;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x800;

            vgmstream->num_samples = ps_bytes_to_samples(stream_size, channel_count);
            break;

        case 0x47435542: /* "GCUB" (also for Wii) */
            vgmstream->coding_type = coding_NGC_DSP;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x8000;

            /* has a full DSP header, but num_samples may vary slighly between channels, so calc manually */
            dsp_read_coefs_be(vgmstream, streamFile, start_offset+0x1c,0x60);
            dsp_read_hist_be(vgmstream, streamFile, start_offset+0x40, 0x60);
            start_offset += 0x60*channel_count;
            stream_size -= 0x60*channel_count;

            vgmstream->num_samples = dsp_bytes_to_samples(stream_size, channel_count);
            break;

        default:
            goto fail;
    }

    vgmstream->num_streams = total_subsongs;
    vgmstream->stream_size = stream_size;

    if (name_offset) {
        read_string(vgmstream->stream_name,name_size, name_offset,streamFile);
    }


    temp_streamFile = setup_zsnd_streamfile(streamFile, start_offset, stream_size); /* fixes last interleave reads */
    if (!temp_streamFile) goto fail;

    if (!vgmstream_open_stream(vgmstream,temp_streamFile,start_offset))
        goto fail;
    close_streamfile(temp_streamFile);
    return vgmstream;

fail:
    close_streamfile(temp_streamFile);
    close_vgmstream(vgmstream);
    return NULL;
}