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


typedef struct {
    int big_endian;

    int is_sab;
    int is_mab;

    int total_subsongs;
    int target_subsong;

    uint16_t mtrl_index;
    uint16_t mtrl_number;
    int loop_flag;
    int channels;
    int codec;
    int sample_rate;
    int loop_start;
    int loop_end;
    off_t mtrl_offset;
    off_t extradata_offset;
    size_t extradata_size;
    size_t stream_size;
    uint16_t extradata_id;

    off_t  filename_offset;
    size_t filename_size;
    off_t  muscname_offset;
    size_t muscname_size;
    off_t  sectname_offset;
    size_t sectname_size;
    off_t  modename_offset;
    size_t modename_size;
    off_t  instname_offset;
    size_t instname_size;
    off_t  sndname_offset;
    size_t sndname_size;

    off_t sections_offset;
    off_t snd_section_offset;
    off_t seq_section_offset;
    off_t trk_section_offset;
    off_t musc_section_offset;
    off_t inst_section_offset;
    off_t mtrl_section_offset;

    char readable_name[STREAM_NAME_SIZE];

} sead_header;

static int parse_sead(sead_header *sead, STREAMFILE *sf);


/* SABF/MABF - Square Enix's "sead" audio games [Dragon Quest Builders (PS3), Dissidia Opera Omnia (mobile), FF XV (PS4)] */
VGMSTREAM* init_vgmstream_sqex_sead(STREAMFILE* sf) {
    VGMSTREAM* vgmstream = NULL;
    sead_header sead = {0};
    off_t start_offset;
    int target_subsong = sf->stream_index;
    uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL;
    uint16_t (*read_u16)(off_t,STREAMFILE*) = NULL;


    /* checks */
    /* .sab: sound/bgm
     * .mab: music
     * .sbin: Dissidia Opera Omnia .sab */
    if (!check_extensions(sf,"sab,mab,sbin"))
        goto fail;

    if (read_u32be(0x00, sf) == 0x73616266) { /* "sabf" */
        sead.is_sab = 1;
    } else if (read_u32be(0x00, sf) == 0x6D616266) { /* "mabf" */
        sead.is_mab = 1;
    } else {
        goto fail;
    }

    /* SEAD handles both sab/mab in the same lib, and other similar files (config, engine, etc).
     * Has some chunks pointing to sections, and each section entry (usually starting with section
     * version/reserved/size) is always padded to 0x10. Most values are unsigned. */


    sead.big_endian = guess_endianness16bit(0x06, sf); /* no flag, use size */
    if (sead.big_endian) {
        read_u32 = read_u32be;
        read_u16 = read_u16be;
    } else {
        read_u32 = read_u32le;
        read_u16 = read_u16le;
    }

    sead.target_subsong = target_subsong;

    if (!parse_sead(&sead, sf))
        goto fail;


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

    vgmstream->meta_type = sead.is_sab ? meta_SQEX_SAB : meta_SQEX_MAB;
    vgmstream->sample_rate = sead.sample_rate;
    vgmstream->num_streams = sead.total_subsongs;
    vgmstream->stream_size = sead.stream_size;
    strcpy(vgmstream->stream_name, sead.readable_name);

    switch(sead.codec) {

        case 0x01: { /* PCM [Chrono Trigger (PC) sfx] */
            start_offset = sead.extradata_offset + sead.extradata_size;

            vgmstream->coding_type = coding_PCM16LE;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x02;

            /* no known extradata */

            vgmstream->num_samples = pcm_bytes_to_samples(sead.stream_size, vgmstream->channels, 16);
            vgmstream->loop_start_sample = sead.loop_start;
            vgmstream->loop_end_sample   = sead.loop_end;
            break;
        }

        case 0x02: { /* MSADPCM [Dragon Quest Builders (Vita) sfx] */
            start_offset = sead.extradata_offset + sead.extradata_size;

            vgmstream->coding_type = coding_MSADPCM;
            vgmstream->layout_type = layout_none;
            vgmstream->frame_size = read_u16(sead.extradata_offset+0x04, sf);

            /* extradata: */
            /* 0x00: version */
            /* 0x01: reserved */
            /* 0x02: size */
            /* 0x02: frame size */
            /* 0x06: reserved */
            /* 0x08: loop start offset */
            /* 0x0c: loop end offset  */

            /* much like AKBs, loop values are slightly different, probably more accurate
             * (if no loop, loop_end doubles as num_samples) */
            vgmstream->num_samples = msadpcm_bytes_to_samples(sead.stream_size, vgmstream->frame_size, vgmstream->channels);
            vgmstream->loop_start_sample = read_u32(sead.extradata_offset+0x08, sf);
            vgmstream->loop_end_sample   = read_u32(sead.extradata_offset+0x0c, sf);
            break;
        }

#ifdef VGM_USE_VORBIS
        case 0x03: { /* VORBIS (Ogg subfile) [Final Fantasy XV Benchmark sfx (PC)] */
            VGMSTREAM *ogg_vgmstream = NULL;
            ogg_vorbis_meta_info_t ovmi = {0};
            off_t subfile_offset = sead.extradata_offset + sead.extradata_size;

            ovmi.meta_type = vgmstream->meta_type;
            ovmi.total_subsongs = sead.total_subsongs;
            ovmi.stream_size = sead.stream_size;

            /* extradata: */
            /* 0x00: version */
            /* 0x01: reserved */
            /* 0x02: size */
            /* 0x04: loop start offset */
            /* 0x08: loop end offset */
            /* 0x0c: num samples */
            /* 0x10: header size */
            /* 0x14: seek table size */
            /* 0x18: reserved x2 */
            /* 0x20: seek table */

            ogg_vgmstream = init_vgmstream_ogg_vorbis_config(sf, subfile_offset, &ovmi);
            if (ogg_vgmstream) {
                ogg_vgmstream->num_streams = vgmstream->num_streams;
                ogg_vgmstream->stream_size = vgmstream->stream_size;
                strcpy(ogg_vgmstream->stream_name, vgmstream->stream_name);

                close_vgmstream(vgmstream);
                return ogg_vgmstream;
            }
            else {
                goto fail;
            }

            break;
        }
#endif

#ifdef VGM_USE_ATRAC9
        case 0x04: { /* ATRAC9 [Dragon Quest Builders (Vita), Final Fantaxy XV (PS4)] */
            atrac9_config cfg = {0};

            start_offset = sead.extradata_offset + sead.extradata_size;

            cfg.channels = vgmstream->channels;
            cfg.config_data = read_u32(sead.extradata_offset+0x0c, sf);
            cfg.encoder_delay = read_u32(sead.extradata_offset+0x18, sf);

            /* extradata: */
            /* 0x00: version */
            /* 0x01: reserved */
            /* 0x02: size */
            /* 0x04: block align */
            /* 0x06: block samples */
            /* 0x08: channel mask */
            /* 0x1c: config */
            /* 0x10: samples */
            /* 0x14: "overlap delay" */
            /* 0x18: "encoder delay" */
            /* 0x1c: sample rate */
            /* 0x24: loop start */
            /* 0x28: loop end */

            vgmstream->codec_data = init_atrac9(&cfg);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_ATRAC9;
            vgmstream->layout_type = layout_none;

            vgmstream->channel_layout = read_u32(sead.extradata_offset+0x08,sf);
            vgmstream->sample_rate = read_u32(sead.extradata_offset+0x1c,sf); /* SAB's sample rate can be different/wrong */
            vgmstream->num_samples = read_u32(sead.extradata_offset+0x10,sf); /* loop values above are also weird and ignored */
            vgmstream->loop_start_sample = read_u32(sead.extradata_offset+0x20, sf) - (sead.loop_flag ? cfg.encoder_delay : 0); //loop_start
            vgmstream->loop_end_sample   = read_u32(sead.extradata_offset+0x24, sf) - (sead.loop_flag ? cfg.encoder_delay : 0); //loop_end
            break;
        }
#endif

#ifdef VGM_USE_MPEG
        case 0x06: { /* MSMP3 (MSF subfile) [Dragon Quest Builders (PS3)] */
            mpeg_custom_config cfg = {0};

            start_offset = sead.extradata_offset + sead.extradata_size;

            /* extradata: */
            /* proper MSF header, but sample rate/loops are ignored in favor of SAB's */

            vgmstream->codec_data = init_mpeg_custom(sf, start_offset, &vgmstream->coding_type, vgmstream->channels, MPEG_STANDARD, &cfg);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->layout_type = layout_none;

            vgmstream->num_samples = mpeg_bytes_to_samples(sead.stream_size, vgmstream->codec_data);
            vgmstream->loop_start_sample = sead.loop_start;
            vgmstream->loop_end_sample = sead.loop_end;
            break;
        }
#endif

        case 0x07: { /* HCA (subfile) [Dissidia Opera Omnia (Mobile), Final Fantaxy XV (PS4)] */
            VGMSTREAM *temp_vgmstream = NULL;
            STREAMFILE *temp_sf = NULL;
            off_t subfile_offset = sead.extradata_offset + 0x10;
            size_t subfile_size = sead.stream_size + sead.extradata_size - 0x10;

            size_t key_start = sead.extradata_id & 0xff;
            size_t header_size = read_u16(sead.extradata_offset+0x02, sf);
            int encryption = read_u8(sead.extradata_offset+0x0d, sf);
            /* encryption type 0x01 found in Final Fantasy XII TZA (PS4/PC)
             * (XOR subtable is fed to HCA's engine instead of generating from a key) */

            /* extradata (mostly same as HCA's): */
            /* 0x00: version */
            /* 0x01: size */
            /* 0x02: header size */
            /* 0x04: frame size */
            /* 0x06: loop start frame */
            /* 0x08: loop end frame */
            /* 0x0a: inserted samples (encoder delay) */
            /* 0x0c: "use mixer" flag */
            /* 0x0d: encryption flag */
            /* 0x0e: reserved x2 */
            /* 0x10+ HCA header */

            temp_sf = setup_sqex_sead_streamfile(sf, subfile_offset, subfile_size, encryption, header_size, key_start);
            if (!temp_sf) goto fail;

            temp_vgmstream = init_vgmstream_hca(temp_sf);
            if (temp_vgmstream) {
                /* loops can be slightly different (~1000 samples) but probably HCA's are more accurate */
                temp_vgmstream->num_streams = vgmstream->num_streams;
                temp_vgmstream->stream_size = vgmstream->stream_size;
                temp_vgmstream->meta_type = vgmstream->meta_type;
                strcpy(temp_vgmstream->stream_name, vgmstream->stream_name);

                close_streamfile(temp_sf);
                close_vgmstream(vgmstream);
                return temp_vgmstream;
            }
            else {
                close_streamfile(temp_sf);
                goto fail;
            }
        }

        case 0x00: /* NONE / dummy entry */
        case 0x05: /* XMA2 (extradata may be a XMA2 fmt extra chunk) */
        case 0x08: /* SWITCHOPUS (no extradata?) */
        default:
            VGM_LOG("SQEX SEAD: unknown codec %x\n", sead.codec);
            goto fail;
    }

    strcpy(vgmstream->stream_name, sead.readable_name);

    /* open the file for reading */
    if ( !vgmstream_open_stream(vgmstream, sf, start_offset) )
        goto fail;
    return vgmstream;

fail:
    close_vgmstream(vgmstream);
    return NULL;
}


static void build_readable_name(char * buf, size_t buf_size, sead_header *sead, STREAMFILE *sf) {

    if (sead->is_sab) {
        char descriptor[255], name[255];

        if (sead->filename_size > 255 || sead->sndname_size > 255) goto fail;

        read_string(descriptor,sead->filename_size+1,sead->filename_offset, sf);
        read_string(name,sead->sndname_size+1,sead->sndname_offset, sf);

        snprintf(buf,buf_size, "%s/%s", descriptor, name);
    }
    else {
        char descriptor[255], name[255], mode[255];

        if (sead->filename_size > 255 || sead->muscname_size > 255 || sead->sectname_size > 255 || sead->modename_size > 255) goto fail;

        read_string(descriptor,sead->filename_size+1,sead->filename_offset, sf);
      //read_string(filename,sead->muscname_size+1,sead->muscname_offset, sf); /* same as filename, not too interesting */
        if (sead->sectname_offset)
            read_string(name,sead->sectname_size+1,sead->sectname_offset, sf);
        else if (sead->instname_offset)
            read_string(name,sead->instname_size+1,sead->instname_offset, sf);
        else
            strcpy(name, "?");
        if (sead->modename_offset > 0)
            read_string(mode,sead->modename_size+1,sead->modename_offset, sf);

        /* default mode in most files */
        if (sead->modename_offset == 0 || strcmp(mode, "Mode") == 0 || strcmp(mode, "Mode0") == 0)
            snprintf(buf,buf_size, "%s/%s", descriptor, name);
        else
            snprintf(buf,buf_size, "%s/%s/%s", descriptor, name, mode);
    }

    return;
fail:
    VGM_LOG("SEAD: bad name found\n");
}

static void parse_sead_mab_name(sead_header *sead, STREAMFILE *sf) {
    uint32_t (*read_u32)(off_t,STREAMFILE*) = sead->big_endian ? read_u32be : read_u32le;
    uint16_t (*read_u16)(off_t,STREAMFILE*) = sead->big_endian ? read_u16be : read_u16le;
    int i, j, k, entries;
    off_t  target_muscname_offset = 0, target_sectname_offset = 0;
    size_t target_muscname_size = 0, target_sectname_size = 0;


    /* find names referencing to our material stream, usually:
     * - music > sections > layers (<> meters) > material index
     * - instruments > instrument materials > material index */


    /* parse musics */
    entries = read_u16(sead->musc_section_offset + 0x04, sf);
    for (i = 0; i < entries; i++) {
        off_t musc_offset = sead->musc_section_offset + read_u32(sead->musc_section_offset + 0x10 + i*0x04, sf);
        off_t muscname_offset, table_offset;
        size_t musc_size, muscname_size;
        int musc_version, sect_count, mode_count;

        musc_version = read_u8 (musc_offset + 0x00, sf);
        /* 0x01: "output"? */
        musc_size    = read_u16(musc_offset + 0x02, sf);
        sect_count   = read_u8 (musc_offset + 0x04, sf);
        mode_count   = read_u8 (musc_offset + 0x05, sf);
        /* 0x06: category? */
        /* 0x07: priority (default 0x80) */
        /* 0x08: number */
        /* 0x0a: flags */
        /* 0x0b: distance attenuation curve? */
        /* 0x0c: interior factor (f32) */
        /* 0x10: name in <=v8 */
        /* 0x20: audible range? (f32) */
        /* 0x24: inner range? (f32) */
        /* 0x28: volume (f32) */
        /* 0x2c: send busses x4 / send volumes (f32) x4 */
        /* 0x40: counts: aux sends/end methods/start methods/zero-ones */
        /* 0x44: sample rate */
        muscname_size  = read_u8(musc_offset + 0x48, sf);
        /* 0x49: port? */
        /* 0x4a: reserved */
        /* 0x4c: audio length (float) */
        /* 0x50+: extra data in later versions */

        if (musc_version <= 8) {
            muscname_offset = musc_offset + 0x10;
            muscname_size = 0x0f;
        }
        else {
            muscname_offset = musc_offset + musc_size;
        }

        table_offset = align_size_to_block(muscname_offset + muscname_size + 0x01, 0x10);

        /* parse sections (layered parts that possibly transition into others using "meter" info) */
        for (j = 0; j < sect_count; j++) {
            off_t sect_offset = musc_offset + read_u32(table_offset + j*0x04, sf);
            off_t sectname_offset, subtable_offset;
            size_t sect_size, sectname_size;
            int sect_version, layr_count;

            sect_version = read_u8 (sect_offset + 0x00, sf);
            /* 0x01: number */
            sect_size    = read_u16(sect_offset + 0x02, sf);

            if (sect_version <= 7) {
                /* 0x04: meter count */
                layr_count = read_u8(sect_offset + 0x05, sf);
                /* 0x06: custom points count */
                /* 0x08: entry points (sample) */
                /* 0x0c: exit points (sample) */
                /* 0x10: loop start */
                /* 0x14: loop end */
                /* 0x18+: meter transition timing info (offsets, points, curves, etc) */
                sectname_offset = 0x30;
                sectname_size = 0x0f;

                subtable_offset = sect_offset + sect_size;
            }
            else {
                sectname_size = read_u8(sect_offset + 0x04, sf);
                layr_count = read_u8(sect_offset + 0x05, sf);
                /* 0x06: custom points count */
                /* 0x08: entry point (sample) */
                /* 0x0c: exit point (sample) */
                /* 0x10: loop start */
                /* 0x14: loop end */
                /* 0x18: meter count */
                /* 0x1c+: meter transition timing info (offsets, points, curves, etc) */
                sectname_offset = sect_offset + sect_size;

                subtable_offset = align_size_to_block(sectname_offset + sectname_size + 0x01, 0x10);
            }


            if (j + 1== sead->target_subsong) {
                target_muscname_offset = muscname_offset;
                target_muscname_size = muscname_size;
                target_sectname_offset = sectname_offset;
                target_sectname_size = sectname_size;
            }

            /* parse layers */
            for (k = 0; k < layr_count; k++) {
                off_t layr_offset = sect_offset + read_u32(subtable_offset + k*0x04, sf);
                int mtrl_index;

                /* 0x00: version */
                /* 0x01: flags */
                /* 0x02: size */
                mtrl_index = read_u16(layr_offset + 0x04, sf);
                /* 0x06: loop count */
                /* 0x08: offset */
                /* 0x0c: end point (sample) */

                if (mtrl_index == sead->mtrl_index) {
                    sead->muscname_offset = muscname_offset;
                    sead->muscname_size   = muscname_size;
                    sead->sectname_offset = sectname_offset;
                    sead->sectname_size = sectname_size;
                    //break;
                }
            }

            /* use last name for cloned materials (see below) */
            //if (sead->sectname_offset > 0)
            //    break;

            /* meters offset go after layer offsets, not useful */
        }

        /* in some files (ex. FF12 TZA) materials are cloned, but sections only point to one of the
         * clones, so one material has multiple section names while others have none. For those cases
         * we can try to match names by subsong number */
        if (sead->sectname_offset == 0) {
            sead->muscname_offset = target_muscname_offset;
            sead->muscname_size   = target_muscname_size;
            sead->sectname_offset = target_sectname_offset;
            sead->sectname_size = target_sectname_size;
            VGM_LOG("MAB: voodoo name matching\n");
        }


        /* modes have names (almost always "Mode" and only 1 entry, rarely "Water" / "Restaurant" / etc)
         * and seem referenced manually (in-game events) and alter sound parameters instead of being
         * an actual playable thing (only found multiple in FFXV's bgm_gardina) */

        /* hack to use mode as subsong name, which for the only known file looks correct */
        if (mode_count == sead->total_subsongs)
            i = sead->target_subsong - 1;
        else
            i = 0;

        { //for (i = 0; i < mode_count; i++) {
            off_t mode_offset = musc_offset + read_u32(table_offset + sect_count*0x04 + i*0x04, sf);
            off_t modename_offset;
            size_t mode_size, modename_size;
            int mode_version;

            mode_version  = read_u8 (mode_offset + 0x00, sf);
            /* 0x01: flags */
            mode_size     = read_u16(mode_offset + 0x02, sf);
            /* 0x04: number */
            modename_size = read_u8 (mode_offset + 0x06, sf);
            /* 0x07: reserved */
            /* 0x08: transition param offset */
            /* 0x0c: reserved */
            /* 0x10: volume */
            /* 0x14: pitch */
            /* 0x18: lowpass */
            /* 0x1c: speed */
            /* 0x20: name <=v2 (otherwise null) */

            if (mode_version <= 2) {
                modename_offset = mode_offset + 0x20;
                modename_size = 0x0f;
            }
            else {
                modename_offset = mode_offset + mode_size;
            }

            sead->modename_offset = modename_offset;
            sead->modename_size = modename_size;
        }
    }


    /* parse instruments (very rare, ex. KH3 tut) */
    entries = read_u16(sead->inst_section_offset + 0x04, sf);
    for (i = 0; i < entries; i++) {
        off_t inst_offset = sead->inst_section_offset + read_u32(sead->inst_section_offset + 0x10 + i*0x04, sf);
        off_t instname_offset, mtrl_offset;
        size_t instname_size;
        int mtrl_count;

        /* 0x00: version */
        /* 0x01: "output"? */
        /* 0x02: size */
        /* 0x04: type (normal, random, switch, etc) */
        mtrl_count = read_u8(inst_offset + 0x05, sf);
        /* 0x06: category */
        /* 0x07: priority */
        /* 0x08: number */
        /* 0x0a: flags */
        /* 0x0b: distance attenuation curve */
        /* 0x0c: interior factor (f32) */
        /* 0x10: audible range (f32) */
        /* 0x14: inner range (f32) */
        /* 0x18: play length (f32) */
        /* 0x1c: reserved */
        /* 0x20: ? (f32) */
        /* 0x30: name */

        instname_offset = inst_offset + 0x30;
        instname_size = 0x0F;

        mtrl_offset = instname_offset + instname_size + 0x01;

        /* parse instrument materials */
        for (j = 0; j < mtrl_count; j++) {
            size_t mtrl_size;
            int mtrl_index;

            /* 0x00: version */
            /* 0x01: value (meaning depends on type) */
            mtrl_size  = read_u16(mtrl_offset + 0x02, sf);
            mtrl_index = read_u16(mtrl_offset + 0x04, sf);
            /* 0x06: number */
            /* 0x08: volume */
            /* 0x0c: sync point */
            /* 0x10: sample rate */
            /* 0x14-20: reserved */

            if (mtrl_index == sead->mtrl_index) {
                sead->instname_offset = instname_offset;
                sead->instname_size = instname_size;
                break;
            }

            mtrl_offset += mtrl_size;
        }

        if (sead->instname_offset > 0)
            break;
    }
}

static void parse_sead_sab_name(sead_header *sead, STREAMFILE *sf) {
    uint32_t (*read_u32)(off_t,STREAMFILE*) = sead->big_endian ? read_u32be : read_u32le;
    uint16_t (*read_u16)(off_t,STREAMFILE*) = sead->big_endian ? read_u16be : read_u16le;
    int i, j, entries;

    /* find names referencing to our material stream, usually:
     * - sound > sequence index
     * - sequence > command > track index
     * - track > material index
     *
     * most of the time sounds<>materials go 1:1 but there are exceptions
     * (ex. DQB se_break_soil, FFXV aircraftzeroone, FFXV 03bt100031pc00)
     *
     * some configs (mainly zero-ones, that seem to be a kind of random sound) have a name too,
     * but it's usually "Default" "(name)ZeroOne" and other uninteresting stuff */

    /* parse sounds */
    entries = read_u16(sead->snd_section_offset + 0x04, sf);
    for (i = 0; i < entries; i++) {
        off_t snd_offset = sead->snd_section_offset + read_u32(sead->snd_section_offset + 0x10 + i*0x04, sf);
        size_t snd_size, sndname_size;
        off_t seqi_start, seqi_offset, sndname_offset;
        int snd_version, seqi_count;

        snd_version = read_u8(snd_offset + 0x00, sf);
        /* 0x01: work? */
        snd_size = read_u16(snd_offset + 0x02, sf);
        /* 0x04: type */
        seqi_count = read_u8(snd_offset + 0x05, sf); /* may be 0 */
        /* 0x06: category */
        /* 0x07: priority (default 0x80) */
        /* 0x08: number */
        /* 0x0a: start+end macro */
        /* 0x0c: volume */
        /* 0x10: cycle config */
        /* 0x1a: header size? */
        seqi_start = read_u16(snd_offset + 0x1a, sf);
        /* 0x1c: audible range */
        /* 0x20: output/curve/port */
        /* 0x23: reserved/name size */
        /* 0x24: play length */
        /* 0x28+: more config params */

        if (snd_version <= 8) {
            sndname_size = 0x0F;
            sndname_offset = snd_offset + 0x50;
        }
        else {
            sndname_size = read_u8(snd_offset + 0x23, sf);
            sndname_offset = snd_offset + snd_size;
        }

        /* parse sequence info */
        seqi_offset = snd_offset + seqi_start;
        for (j = 0; j < seqi_count; j++) {
            size_t seqi_size;
            int seq_index;

            /* 0x00: version */
            /* 0x01: reserved */
            seqi_size = read_u16(seqi_offset + 0x02, sf);
            seq_index = read_u16(seqi_offset + 0x04, sf);
            /* 0x06: sequence ID */
            /* 0x08: reserved x2 */

            seqi_offset += seqi_size;

            /* parse sequence */
            {
                off_t seq_offset = sead->seq_section_offset + read_u32(sead->seq_section_offset + 0x10 + seq_index*0x04, sf);
                off_t cmnd_start, cmnd_offset;
                int seq_version;

                seq_version = read_u8(seq_offset + 0x00, sf);
                /* 0x01: reserved */
                /* 0x02: size */

                if (seq_version <= 2) {
                    /* 0x04: config depending on type */
                    /* 0x10: number */
                    /* 0x12: volume zero-one offsets */
                    /* 0x12: pitch zero-one offsets */
                    cmnd_start = read_u16(seq_offset + 0x16, sf);
                    /* 0x18: reserved x2 */
                }
                else {
                    /* 0x04: id */
                    cmnd_start = read_u16(seq_offset + 0x06, sf);
                    /* 0x08: zero-ones count */
                    /* 0x09: reserved */
                    /* 0x10: config depending on type */
                }


                /* parse sequence commands (breaks once an end command is found) */
                cmnd_offset = seq_offset + cmnd_start;
                while (cmnd_offset < sead->trk_section_offset) {
                    uint8_t cmnd_size, cmnd_type, cmnd_body;

                    /* 0x00: version (each command may have a different version) */
                    cmnd_size = read_u8(cmnd_offset + 0x01, sf); /* doesn't include body */
                    cmnd_type = read_u8(cmnd_offset + 0x02, sf);
                    cmnd_body = read_u8(cmnd_offset + 0x03, sf);

                    /* 0=none, 1=end, 2=key on, 3=interval, 4=keyoff, 5=wat, 6=loop start, 7=loop end, 8=trigger */
                    if (cmnd_type == 0x02) {
                        uint32_t trk_index;

                        trk_index = read_u32(cmnd_offset + cmnd_size + 0x00, sf);
                        /* 0x04: is loop */
                        /* 0x05: reserved */
                        /* 0x06: track id */
                        /* 0x08: play length (f32) */

                        /* parse track */
                        {
                            off_t trk_offset = sead->trk_section_offset + read_u32(sead->trk_section_offset + 0x10 + trk_index*0x04, sf);
                            uint8_t trk_type;
                            uint16_t mtrl_index;

                            /* 0x00: version */
                            trk_type = read_u8(trk_offset + 0x01, sf);
                            /* 0x02: size */
                            /* 0x04: config depending on type */
                            /* 0x08: id */
                            /* 0x0a: child id */
                            /* 0x0c: reserved */

                            /* 0=none, 1=material, 2=sound */
                            if (trk_type == 0x01) {
                                mtrl_index = read_u16(trk_offset + 0x04, sf);
                                /* 0x02: bank */

                                /* assumes same bank, not sure if bank info is even inside .sab */
                                if (mtrl_index == sead->mtrl_index) {
                                    sead->sndname_offset = sndname_offset;
                                    sead->sndname_size = sndname_size;
                                }
                            }
                            else if (trk_type == 0x02) {
                                /* 0x00: index */
                                /* 0x02: reserved */
                                /* parse sound again? */
                            }
                        }
                    }

                    cmnd_offset += cmnd_size + cmnd_body;

                    /* commands normally end when a type 0=none is found */
                    if (cmnd_type <= 0x00 || cmnd_type > 0x08)
                        break;
                    if (sead->sndname_offset > 0)
                        break;
                }
            }

        }
    }
}


static int parse_sead(sead_header* sead, STREAMFILE* sf) {
    uint32_t (*read_u32)(off_t,STREAMFILE*) = sead->big_endian ? read_u32be : read_u32le;
    uint16_t (*read_u16)(off_t,STREAMFILE*) = sead->big_endian ? read_u16be : read_u16le;

    /** base header **/
    /* 0x00: id */
    /* 0x04: version (usually 0x02, rarely 0x01 ex FF XV early songs) */
    /* 0x05: flags */
    /* 0x06: size */
    /* 0x08: chunk count (in practice mab=3, sab=4) */
    sead->filename_size = read_u8(0x09, sf);
    /* 0x0a: number (shared between multiple sab/mab though) */
    if (read_u32(0x0c, sf) != get_streamfile_size(sf))
        goto fail;

    /* not set/reserved when version == 1 (name is part of size) */
    if (sead->filename_size == 0)
        sead->filename_size = 0x0f;
    sead->filename_offset = 0x10; /* file description ("BGM", "Music2", "SE", etc, long names are ok) */
    sead->sections_offset = sead->filename_offset + (sead->filename_size + 0x01); /* string null matters for padding */
    sead->sections_offset = align_size_to_block(sead->sections_offset, 0x10);

    /* roughly, mab play audio by calling musics/instruments, and sab with sounds/sequences/track. Both
     * point to a "material" or stream wave that may be reused. We only want material streams as subsongs,
     * but also try to find names from musics/sounds/etc. */

    /** chunk table elements (offsets to sections) **/
    /* 0x00: id */
    /* 0x04: version */
    /* 0x05: reserved */
    /* 0x06: size */
    /* 0x08: offset */
    /* 0x0c: reserved */
    if (sead->is_sab) {
        if (read_u32be(sead->sections_offset + 0x00, sf) != 0x736E6420) goto fail; /* "snd " (sounds) */
        if (read_u32be(sead->sections_offset + 0x10, sf) != 0x73657120) goto fail; /* "seq " (sequences) */
        if (read_u32be(sead->sections_offset + 0x20, sf) != 0x74726B20) goto fail; /* "trk " (tracks) */
        if (read_u32be(sead->sections_offset + 0x30, sf) != 0x6D74726C) goto fail; /* "mtrl" (material headers/streams) */
        sead->snd_section_offset  = read_u32(sead->sections_offset + 0x08, sf);
        sead->seq_section_offset  = read_u32(sead->sections_offset + 0x18, sf);
        sead->trk_section_offset  = read_u32(sead->sections_offset + 0x28, sf);
        sead->mtrl_section_offset = read_u32(sead->sections_offset + 0x38, sf);
    }
    else if (sead->is_mab) {
        if (read_u32be(sead->sections_offset + 0x00, sf) != 0x6D757363) goto fail; /* "musc" (musics) */
        if (read_u32be(sead->sections_offset + 0x10, sf) != 0x696E7374) goto fail; /* "inst" (instruments) */
        if (read_u32be(sead->sections_offset + 0x20, sf) != 0x6D74726C) goto fail; /* "mtrl" (material headers/streams) */
        sead->musc_section_offset = read_u32(sead->sections_offset + 0x08, sf);
        sead->inst_section_offset = read_u32(sead->sections_offset + 0x18, sf);
        sead->mtrl_section_offset = read_u32(sead->sections_offset + 0x28, sf);
    }
    else {
        goto fail;
    }


    /* section "chunk" format at offset: */
    /* 0x00: version */
    /* 0x01: reserved */
    /* 0x02: size */
    /* 0x04: entries */
    /* 0x06: reserved */
    /* 0x10+ 0x04*entry: offset to entry from table start */


    /* find target material offset */
    {
        int i, entries;

        entries = read_u16(sead->mtrl_section_offset + 0x04, sf);

        if (sead->target_subsong == 0) sead->target_subsong = 1;
        sead->total_subsongs = 0;
        sead->mtrl_offset = 0;

        /* manually find subsongs as entries can be dummy (ex. sfx banks in Dissidia Opera Omnia) */
        for (i = 0; i < entries; i++) {
            off_t entry_offset = sead->mtrl_section_offset + read_u32(sead->mtrl_section_offset + 0x10 + i*0x04, sf);

            if (read_u8(entry_offset + 0x05, sf) == 0) {
                continue; /* codec 0 when dummy (see stream header) */
            }

            sead->total_subsongs++;
            if (!sead->mtrl_offset && sead->total_subsongs == sead->target_subsong) {
                sead->mtrl_offset = entry_offset;
                sead->mtrl_index = i;
            }
        }

        /* SAB can contain 0 entries too */
        if (sead->mtrl_offset == 0) {
            vgm_logi("SQEX SEAD: bank has no subsongs (ignore)\n");
            goto fail;
        }
    }

    /** stream header **/
    /* 0x00: version */
    /* 0x01: reserved */
    /* 0x02: size */
    sead->channels      = read_u8 (sead->mtrl_offset + 0x04, sf);
    sead->codec         = read_u8 (sead->mtrl_offset + 0x05, sf); /* format */
    sead->mtrl_number   = read_u16(sead->mtrl_offset + 0x06, sf); /* 0..N */
    sead->sample_rate   = read_u32(sead->mtrl_offset + 0x08, sf);
    sead->loop_start    = read_u32(sead->mtrl_offset + 0x0c, sf); /* in samples but usually ignored */
    sead->loop_end      = read_u32(sead->mtrl_offset + 0x10, sf);
    sead->extradata_size= read_u32(sead->mtrl_offset + 0x14, sf); /* including subfile header, can be 0 */
    sead->stream_size   = read_u32(sead->mtrl_offset + 0x18, sf); /* not including subfile header */
    sead->extradata_id  = read_u16(sead->mtrl_offset + 0x1c, sf);
    /* 0x1e: reserved */

    sead->loop_flag       = (sead->loop_end > 0);
    sead->extradata_offset = sead->mtrl_offset + 0x20;

    if (sead->is_sab) {
        parse_sead_sab_name(sead, sf);
    }
    else if (sead->is_mab) {
        parse_sead_mab_name(sead, sf);
    }

    build_readable_name(sead->readable_name, sizeof(sead->readable_name), sead, sf);

    return 1;
fail:
    return 0;
}
