#include "meta.h"
#include "../coding/coding.h"
#include "../util/m2_psb.h"
#include "../layout/layout.h"


#define PSB_MAX_LAYERS 2

typedef enum { PCM, RIFF_AT3, XMA2, MSADPCM, XWMA, DSP, OPUSNX, RIFF_AT9, VAG } psb_codec_t;
typedef struct {
    const char* id; /* format */
    const char* spec; /* platform */
    const char* ext; /* codec extension (not always) */
    const char* voice; /* base name (mandatory) */
    const char* file; /* original name, often but not always same as voice (optional?) */
    const char* uniq; /* unique name, typically same as file without extension (optional)  */
    const char* wav; /* same as file (optional) */
} psb_temp_t;
typedef struct {
    psb_temp_t* tmp;
    psb_codec_t codec;
    char readable_name[STREAM_NAME_SIZE];

    int total_subsongs;
    int target_subsong;

    /* chunks references */
    uint32_t stream_offset[PSB_MAX_LAYERS];
    uint32_t stream_size[PSB_MAX_LAYERS];
    uint32_t body_offset;
    uint32_t body_size;
    uint32_t intro_offset;
    uint32_t intro_size;
    uint32_t fmt_offset;
    uint32_t fmt_size;
    uint32_t dpds_offset;
    uint32_t dpds_size;

    int layers;
    int channels;
    int format;
    int sample_rate;
    int block_size;
    int avg_bitrate;
    int bps;

    int32_t num_samples;
    int32_t body_samples;
    int32_t intro_samples;
    int32_t skip_samples;
    int loop_flag;
    int32_t loop_start;
    int32_t loop_end;
    int duration_test;

} psb_header_t;


static int parse_psb(STREAMFILE* sf, psb_header_t* psb);


static segmented_layout_data* build_segmented_psb_opus(STREAMFILE* sf, psb_header_t* psb);
static layered_layout_data* build_layered_psb(STREAMFILE* sf, psb_header_t* psb);


/* PSB - M2 container [Sega Vintage Collection (multi), Legend of Mana (multi)] */
VGMSTREAM* init_vgmstream_psb(STREAMFILE* sf) {
    VGMSTREAM* vgmstream = NULL;
    psb_header_t psb = {0};


    /* checks */
    if (!is_id32be(0x00,sf, "PSB\0"))
        goto fail;
    if (!check_extensions(sf, "psb"))
        goto fail;

    if (!parse_psb(sf, &psb))
        goto fail;


    /* handle subfiles */
    {
        const char* ext = NULL;
        VGMSTREAM* (*init_vgmstream)(STREAMFILE* sf) = NULL;

        switch(psb.codec) {
            case RIFF_AT3:   /* Sega Vintage Collection (PS3) */
                ext = "at3";
                init_vgmstream = init_vgmstream_riff;
                break;

            case VAG: /* Plastic Memories (Vita), Judgment (PS4) */
                ext = "vag";
                init_vgmstream = init_vgmstream_vag;
                break;

            case RIFF_AT9: /* Plastic Memories (Vita) */
                ext = "at9";
                init_vgmstream = init_vgmstream_riff;
                break;

            default:
                break;
        }

        if (init_vgmstream != NULL) {
            STREAMFILE* temp_sf = setup_subfile_streamfile(sf, psb.stream_offset[0], psb.stream_size[0], ext);
            if (!temp_sf) goto fail;

            vgmstream = init_vgmstream(temp_sf);
            close_streamfile(temp_sf);
            if (!vgmstream) goto fail;

            vgmstream->num_streams = psb.total_subsongs;
            strncpy(vgmstream->stream_name, psb.readable_name, STREAM_NAME_SIZE);
            return vgmstream;
        }
    }


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

    vgmstream->meta_type = meta_PSB;
    vgmstream->sample_rate = psb.sample_rate;
    vgmstream->num_samples = psb.num_samples;
    vgmstream->loop_start_sample = psb.loop_start;
    vgmstream->loop_end_sample = psb.loop_end;
    vgmstream->num_streams = psb.total_subsongs;
    vgmstream->stream_size = psb.stream_size[0];

    switch(psb.codec) {
        case PCM:
            if (psb.layers > 1) {
                /* somehow R offset can go before L, use layered */
                vgmstream->layout_data = build_layered_psb(sf, &psb);
                if (!vgmstream->layout_data) goto fail;
                vgmstream->layout_type = layout_layered;

                if (!vgmstream->num_samples)
                    vgmstream->num_samples = pcm_bytes_to_samples(psb.stream_size[0], 1, psb.bps);
            }
            else {
                vgmstream->layout_type = layout_interleave;
                vgmstream->interleave_block_size = psb.block_size / psb.channels;
                if (!vgmstream->num_samples)
                    vgmstream->num_samples = pcm_bytes_to_samples(psb.stream_size[0], psb.channels, psb.bps);
            }

            switch(psb.bps) {
                case 16: vgmstream->coding_type = coding_PCM16LE; break; /* Legend of Mana (PC), Namco Museum Archives Vol.1 (PC) */
                case 24: vgmstream->coding_type = coding_PCM24LE; break; /* Legend of Mana (PC) */
                default: goto fail;
            }

            if (psb.duration_test && psb.loop_start + psb.loop_end < vgmstream->num_samples)
                vgmstream->loop_end_sample += psb.loop_start;
            break;

        case MSADPCM: /* [Senxin Aleste (AC)] */
            vgmstream->coding_type = coding_MSADPCM;
            vgmstream->layout_type = layout_none;
            vgmstream->frame_size = psb.block_size;
            if (!vgmstream->num_samples)
                vgmstream->num_samples = msadpcm_bytes_to_samples(psb.stream_size[0], psb.block_size, psb.channels);
            break;

#ifdef VGM_USE_FFMPEG
        case XWMA: { /* [Senxin Aleste (AC)] */
            vgmstream->codec_data = init_ffmpeg_xwma(sf, psb.stream_offset[0], psb.stream_size[0], psb.format, psb.channels, psb.sample_rate, psb.avg_bitrate, psb.block_size);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;

            if (!vgmstream->num_samples) {
                vgmstream->num_samples = xwma_dpds_get_samples(sf, psb.dpds_offset, psb.dpds_size, psb.channels, 0);
            }

            break;
        }

        case XMA2: { /* Sega Vintage Collection (X360) */
            uint8_t buf[0x100];
            size_t bytes;

            bytes = ffmpeg_make_riff_xma_from_fmt_chunk(buf, sizeof(buf), psb.fmt_offset, psb.fmt_size, psb.stream_size[0], sf, 1);
            vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf, bytes, psb.stream_offset[0], psb.stream_size[0]);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;

            xma_fix_raw_samples(vgmstream, sf, psb.stream_offset[0], psb.stream_size[0], psb.fmt_offset, 1,1);
            break;
        }

        case OPUSNX: { /* Legend of Mana (Switch) */
            vgmstream->layout_data = build_segmented_psb_opus(sf, &psb);
            if (!vgmstream->layout_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_segmented;
            break;
        }
#endif

        case DSP: /* Legend of Mana (Switch) */
            /* standard DSP resources */
            if (psb.layers > 1) {
                /* somehow R offset can go before L, use layered */
                vgmstream->layout_data = build_layered_psb(sf, &psb);
                if (!vgmstream->layout_data) goto fail;
                vgmstream->coding_type = coding_NGC_DSP;
                vgmstream->layout_type = layout_layered;
            }
            else {
                vgmstream->coding_type = coding_NGC_DSP;
                vgmstream->layout_type = layout_none;

                dsp_read_coefs_le(vgmstream,sf, psb.stream_offset[0] + 0x1c, 0);
                dsp_read_hist_le(vgmstream,sf, psb.stream_offset[0] + 0x1c + 0x20, 0);
            }

            vgmstream->num_samples = read_u32le(psb.stream_offset[0] + 0x00, sf);
            if (psb.duration_test && psb.loop_start + psb.loop_end < vgmstream->num_samples)
                vgmstream->loop_end_sample += psb.loop_start;
            break;

        default:
            goto fail;
    }

    strncpy(vgmstream->stream_name, psb.readable_name, STREAM_NAME_SIZE);

    if (!vgmstream_open_stream(vgmstream, sf, psb.stream_offset[0]))
        goto fail;
    return vgmstream;

fail:
    close_vgmstream(vgmstream);
    return NULL;
}

static segmented_layout_data* build_segmented_psb_opus(STREAMFILE* sf, psb_header_t* psb) {
    segmented_layout_data* data = NULL;
    int i, pos = 0, segment_count = 0, max_count = 2;

    //TODO improve
    //TODO these use standard switch opus (VBR), could sub-file? but skip_samples becomes more complex

    uint32_t offsets[] = {psb->intro_offset, psb->body_offset};
    uint32_t sizes[] = {psb->intro_size, psb->body_size};
    uint32_t samples[] = {psb->intro_samples, psb->body_samples};
    uint32_t skips[] = {0, psb->skip_samples};

    /* intro + body (looped songs) or just body (standard songs)
       in full loops intro is 0 samples with a micro 1-frame opus [Nekopara (Switch)] */
    if (offsets[0] && samples[0])
        segment_count++;
    if (offsets[1] && samples[1])
        segment_count++;

    /* init layout */
    data = init_layout_segmented(segment_count);
    if (!data) goto fail;

    for (i = 0; i < max_count; i++) {
        if (!offsets[i] || !samples[i])
            continue;
#ifdef VGM_USE_FFMPEG
       {
            int start = read_u32le(offsets[i] + 0x10, sf) + 0x08;
            int skip = read_s16le(offsets[i] + 0x1c, sf);

            VGMSTREAM* v = allocate_vgmstream(psb->channels, 0);
            if (!v) goto fail;

            data->segments[pos++] = v;
            v->sample_rate = psb->sample_rate;
            v->num_samples = samples[i];
            v->codec_data = init_ffmpeg_switch_opus(sf, offsets[i] + start, sizes[i] - start, psb->channels, skips[i] + skip, psb->sample_rate);
            if (!v->codec_data) goto fail;
            v->coding_type = coding_FFmpeg;
            v->layout_type = layout_none;
        }
#else
        goto fail;
#endif
    }

    if (!setup_layout_segmented(data))
        goto fail;

    return data;
fail:
    free_layout_segmented(data);
    return NULL;
}


static VGMSTREAM* try_init_vgmstream(STREAMFILE* sf, init_vgmstream_t init_vgmstream, const char* extension, uint32_t offset, uint32_t size) {
    STREAMFILE* temp_sf = NULL;
    VGMSTREAM* v = NULL;

    temp_sf = setup_subfile_streamfile(sf, offset, size, extension);
    if (!temp_sf) goto fail;

    v = init_vgmstream(temp_sf);
    close_streamfile(temp_sf);
    return v;
fail:
    return NULL;
}

static layered_layout_data* build_layered_psb(STREAMFILE* sf, psb_header_t* psb) {
    layered_layout_data* data = NULL;
    int i;


    /* init layout */
    data = init_layout_layered(psb->layers);
    if (!data) goto fail;

    for (i = 0; i < psb->layers; i++) {
        switch (psb->codec) {
            case PCM: {
                VGMSTREAM* v = allocate_vgmstream(1, 0);
                if (!v) goto fail;

                data->layers[i] = v;

                v->sample_rate = psb->sample_rate;
                v->num_samples = psb->num_samples;

                switch(psb->bps) {
                    case 16: v->coding_type = coding_PCM16LE; break;
                    case 24: v->coding_type = coding_PCM24LE; break;
                    default: goto fail;
                }
                v->layout_type = layout_none;
                if (!v->num_samples)
                    v->num_samples = pcm_bytes_to_samples(psb->stream_size[i], 1, psb->bps);

                if (!vgmstream_open_stream(v, sf, psb->stream_offset[i]))
                    goto fail;
                break;
            }

            case DSP:
                data->layers[i] = try_init_vgmstream(sf, init_vgmstream_ngc_dsp_std_le, "adpcm", psb->stream_offset[i], psb->stream_size[i]);
                if (!data->layers[i]) goto fail;
                break;

            default:
                VGM_LOG("psb: layer not implemented\n");
                goto fail;
        }
    }

    /* setup layered VGMSTREAMs */
    if (!setup_layout_layered(data))
        goto fail;
    return data;
fail:
    free_layout_layered(data);
    return NULL;
}


/*****************************************************************************/

static int prepare_fmt(STREAMFILE* sf, psb_header_t* psb) {
    uint32_t offset = psb->fmt_offset;
    if (!offset)
        return 1; /* other codec, probably */

    psb->format         = read_u16le(offset + 0x00,sf);
    if (psb->format == 0x6601) { /* X360 */
        psb->format         = read_u16be(offset + 0x00,sf);
        psb->channels       = read_u16be(offset + 0x02,sf);
        psb->sample_rate    = read_u32be(offset + 0x04,sf);
        xma2_parse_fmt_chunk_extra(sf,
            offset,
            &psb->loop_flag,
            &psb->num_samples,
            &psb->loop_start,
            &psb->loop_end,
            1);
    }
    else {
        psb->channels       = read_u16le(offset + 0x02,sf);
        psb->sample_rate    = read_u32le(offset + 0x04,sf);
        psb->avg_bitrate    = read_u32le(offset + 0x08,sf);
        psb->block_size     = read_u16le(offset + 0x0c,sf);
        psb->bps            = read_u16le(offset + 0x0e,sf);
        /* 0x10+ varies */

        switch(psb->format) {
            case 0x0002:
                if (!msadpcm_check_coefs(sf, offset + 0x14))
                    goto fail;
                break;
            default:
                break;
        }

    }

    return 1;
fail:
    return 0;
}

static int prepare_codec(STREAMFILE* sf, psb_header_t* psb) {
    const char* spec = psb->tmp->spec;
    const char* ext = psb->tmp->ext;

    /* try fmt (most common) */
    if (psb->format != 0) {
        switch(psb->format) {
            case 0x01:
                psb->codec = PCM;
                break;
            case 0x02:
                psb->codec = MSADPCM;
                break;
            case 0x161:
                psb->codec = XWMA;
                break;
            case 0x166:
                psb->codec = XMA2;
                break;
            default:
                goto fail;
        }
        return 1;
    }

    /* try console strings */
    if (!spec)
        goto fail;

    if (strcmp(spec, "nx") == 0) {
        if (!ext)
            goto fail;

        /* common, multichannel */
        if (strcmp(ext, ".opus") == 0) {
            psb->codec = OPUSNX;

            psb->body_samples -= psb->skip_samples;
            if (!psb->loop_flag)
                psb->loop_flag = psb->intro_samples > 0;
            psb->loop_start = psb->intro_samples;
            psb->loop_end = psb->body_samples + psb->intro_samples;
            psb->num_samples = psb->intro_samples + psb->body_samples;
            return 1;
        }

        /* Legend of Mana (Switch), layered */
        if (strcmp(ext, ".adpcm") == 0) {
            psb->codec = DSP;

            psb->channels = psb->layers;
            return 1;
        }

        /* Castlevania Advance Collection (Switch), layered */
        if (strcmp(ext, ".p16") == 0) {
            psb->codec = PCM;
            psb->bps = 16;

            psb->channels = psb->layers;
            return 1;
        }
    }

    if (strcmp(spec, "ps3") == 0) {
        psb->codec = RIFF_AT3;
        return 1;
    }

    if (strcmp(spec, "vita") == 0 || strcmp(spec, "ps4") == 0) {
        if (is_id32be(psb->stream_offset[0], sf, "RIFF"))
            psb->codec = RIFF_AT9;
        else
            psb->codec = VAG;
        return 1;
    }

fail:
    vgm_logi("PSB: unknown codec (report)\n");
    return 0;
}


static int prepare_name(psb_header_t* psb) {
    char* buf = psb->readable_name;
    int buf_size = sizeof(psb->readable_name);
    const char* main_name = psb->tmp->voice;
    const char* sub_name = psb->tmp->uniq;
    int main_len;

    if (!sub_name)
        sub_name = psb->tmp->wav;
    if (!sub_name)
        sub_name = psb->tmp->file;

    if (!main_name) /* shouldn't happen */
        return 1;

    /* sometimes we have main="bgm01", sub="bgm01.wav" = detect and ignore */
    main_len = strlen(main_name);
    if (sub_name && strncmp(main_name, sub_name, main_len) == 0) {
        if (sub_name[main_len] == '\0' || strcmp(sub_name + main_len, ".wav") == 0)
            sub_name = NULL;
    }

    if (sub_name) {
        snprintf(buf, buf_size, "%s/%s", main_name, sub_name);
    }
    else {
        snprintf(buf, buf_size, "%s", main_name);
    }

    return 1;
}

static int prepare_psb_extra(STREAMFILE* sf, psb_header_t* psb) {
    if (!prepare_fmt(sf, psb))
        goto fail;
    if (!prepare_codec(sf, psb))
        goto fail;
    if (!prepare_name(psb))
        goto fail;
    return 1;
fail:
    return 0;
}


/* channelList is an array (N layers, though typically only mono codecs like DSP) of objects:
 * - archData: resource offset (RIFF) or sub-object
 *   - data/fmt/loop/wav
 *   - data/ext/samprate
 *   - body/channelCount/ext/intro/loop/samprate [Legend of Mana (Switch)]
 *     - body: data/sampleCount/skipSampleCount, intro: data/sampleCount
 *   - data/dpds/fmt/wav/loop
 * - pan: array [N.0 .. 0.N] (when N layers, in practice just a wonky L/R definition)
 */
static int parse_psb_channels(psb_header_t* psb, psb_node_t* nchans) {
    int i;
    psb_node_t nchan, narch, nsub, node;

    psb->layers = psb_node_get_count(nchans);
    if (psb->layers == 0) goto fail;
    if (psb->layers > PSB_MAX_LAYERS) goto fail;

    for (i = 0; i < psb->layers; i++) {
        psb_data_t data;
        psb_type_t type;

        psb_node_by_index(nchans, i, &nchan);

        /* try to get possible keys (without overwritting), results will be handled and validated later as combos get complex */
        psb_node_by_key(&nchan, "archData", &narch);
        type = psb_node_get_type(&narch);
        switch (type) {
            case PSB_TYPE_DATA: /* Sega Vintage Collection (PS3) */
                data = psb_node_get_result(&narch).data;
                psb->stream_offset[i] = data.offset;
                psb->stream_size[i] = data.size;
                break;

            case PSB_TYPE_OBJECT: /* rest */
                /* typically:
                 * - data + fmt + others
                 * - body {data + fmt} + intro {data + fmt} + others [Legend of Mana (Switch)]
                 */

                data = psb_node_get_data(&narch, "data");
                if (data.offset) {
                    psb->stream_offset[i] = data.offset;
                    psb->stream_size[i] = data.size;
                }

                data = psb_node_get_data(&narch, "fmt");
                if (data.offset) {
                    psb->fmt_offset = data.offset;
                    psb->fmt_size = data.size;
                }

                if (psb_node_by_key(&narch, "loop", &node)) {
                    /* can be found as "false" with body+intro */
                    if (psb_node_get_type(&node) == PSB_TYPE_ARRAY) {
                        //todo improve
                        psb_node_by_index(&node, 0, &nsub);
                        psb->loop_start = psb_node_get_result(&nsub).num;

                        psb_node_by_index(&node, 1, &nsub);
                        psb->loop_end = psb_node_get_result(&nsub).num + 1; /* assumed, matches num_samples */
                        /* duration [LoM (PC), Namco Museum V1 (PC)] or standard [G-Darius (Sw)] (no apparent flags) */
                        psb->duration_test = 1;
                    }
                }

                if (psb_node_by_key(&narch, "body", &node)) {
                    data = psb_node_get_data(&node, "data");
                    psb->body_offset = data.offset;
                    psb->body_size = data.size;
                    psb->body_samples = psb_node_get_integer(&node, "sampleCount");
                    psb->skip_samples = psb_node_get_integer(&node, "skipSampleCount");
                }

                if (psb_node_by_key(&narch, "intro", &node)) {
                    data = psb_node_get_data(&node, "data");
                    psb->intro_offset = data.offset;
                    psb->intro_size = data.size;
                    psb->intro_samples = psb_node_get_integer(&node, "sampleCount");
                }

                data = psb_node_get_data(&narch, "dpds");
                if (data.offset) {
                    psb->dpds_offset = data.offset;
                    psb->dpds_size = data.size;
                }

                psb->channels = psb_node_get_integer(&narch, "channelCount");

                psb->sample_rate = (int)psb_node_get_float(&narch, "samprate"); /* seen in DSP */
                if (!psb->sample_rate)
                    psb->sample_rate = psb_node_get_integer(&narch, "samprate"); /* seen in OpusNX */

                psb->tmp->ext = psb_node_get_string(&narch, "ext"); /* appears for all channels, assumed to be the same */

                psb->tmp->wav = psb_node_get_string(&narch, "wav");

                /* DSP has a "pan" array like: [1.0, 0.0]=L, [0.0, 1.0 ]=R */
                if (psb_node_by_key(&narch, "pan", &node)) {

                    psb_node_by_index(&node, i, &nsub);
                    if (psb_node_get_result(&nsub).flt != 1.0f) {
                        vgm_logi("PSB: unexpected pan (report)\n");
                    };
                }

                /* background: false?
                 */
                break;

            default:
                goto fail;
        }
    }
    return 1;
fail:
    VGM_LOG("psb: can't parse channel\n");
    return 0;
}


/* parse a single archive, that can contain extra info here or inside channels */
static int parse_psb_voice(psb_header_t* psb, psb_node_t* nvoice) {
    psb_node_t nsong, nchans;


    psb->total_subsongs = psb_node_get_count(nvoice);
    if (psb->target_subsong == 0) psb->target_subsong = 1;
    if (psb->total_subsongs <= 0 || psb->target_subsong > psb->total_subsongs) goto fail;


    /* target voice and stream info */
    if (!psb_node_by_index(nvoice, psb->target_subsong - 1, &nsong))
        goto fail;
    psb->tmp->voice = psb_node_get_key(nvoice, psb->target_subsong - 1);

    psb_node_by_key(&nsong, "channelList", &nchans);
    if (!parse_psb_channels(psb, &nchans))
        goto fail;


    /* unsure of meaning but must exist (usually 0/1) */
    if (psb_node_exists(&nsong, "device") <= 0)
        goto fail;

    /* names (optional) */
    psb->tmp->file = psb_node_get_string(&nsong, "file");
    psb->tmp->uniq = psb_node_get_string(&nsong, "uniq");

    /* optional loop flag (loop points go in channels, or implicit in fmt/RIFF) */
    if (!psb->loop_flag) {
        psb->loop_flag = psb_node_get_integer(&nsong, "loop") > 1;
        /* There is also loopstr/loopStr = "all" when "loop"=2 and "none" when "loop"=0
         * SFX set loop=0, and sometimes songs that look like they could do full loops do too */
    }

    /* other optional keys:
     * - quality: ? (1=MSADPCM, 2=OPUSNX/PCM)
     * - priority: f32, -1.0, 1.0 or 10.0 = max?
     * - type: 0/1? (internal classification?)
     * - volume: 0.0 .. 1.0
     * - group?
     */

    return 1;
fail:
    VGM_LOG("psb: can't parse voice\n");
    return 0;
}

/* .psb is binary JSON-like structure that can be used to hold various formats, we want audio data:
 * - (root): (object)
 *   - "id": (format string)
 *   - "spec": (platform string)
 *   - "version": (float)
 *   - "voice": (objects, one per subsong)
 *     - (voice name 1): (object)
 *       - "channelList": (array of N objects)
 *          - "archData": (main audio part, varies per game/platform/codec)
 *          - "device": ?
 *     ...
 *     - (voice name N): ...
 * From decompilations, audio code reads common keys up to "archData", then depends on game (not unified).
 * Keys are (seemingly) stored in text order.
 */
static int parse_psb(STREAMFILE* sf, psb_header_t* psb) {
    psb_temp_t tmp;
    psb_context_t* ctx = NULL;
    psb_node_t nroot, nvoice;
    float version;

    psb->tmp = &tmp;
    psb->target_subsong = sf->stream_index;

    ctx = psb_init(sf);
    if (!ctx) goto fail;
    //psb_print(ctx);

    /* main process */
    psb_get_root(ctx, &nroot);

    /* format definition, non-audio IDs include "motion", "font", or no "id" at all */
    psb->tmp->id = psb_node_get_string(&nroot, "id");
    if (!psb->tmp->id || strcmp(psb->tmp->id, "sound_archive") != 0) {
        /* "sound" is just a list of available "sound_archive" */
        if (psb->tmp->id && strcmp(psb->tmp->id, "sound") == 0)
            vgm_logi("PSB: empty archive type '%s' (ignore)\n", psb->tmp->id);
        else
            vgm_logi("PSB: unsupported archive type '%s' (ignore?)\n", psb->tmp->id);
        goto fail;
    }

    /* platform: x360/ps3/win/nx/etc */
    psb->tmp->spec = psb_node_get_string(&nroot, "spec");

    /* enforced by M2 code */
    version = psb_node_get_float(&nroot, "version");
    if (version < 1.02f || version > 1.02f) {
        vgm_logi("PSB: unsupported version %f (report)\n", version);
        goto fail;
    }

    /* main subsong */
    psb_node_by_key(&nroot, "voice", &nvoice);
    if (!parse_psb_voice(psb, &nvoice))
        goto fail;

    /* post stuff before closing PSB */
    if (!prepare_psb_extra(sf, psb))
        goto fail;

    psb->tmp = NULL;
    psb_close(ctx);
    return 1;
fail:
    psb_close(ctx);
    VGM_LOG("psb: can't parse PSB\n");
    return 0;
}

#if 0
typedef struct {
    void* init;
    const char* id32;
    const char* exts;
} metadef_t;

metadef_t md_psb = {
    .init = init_vgmstream_psb,
    .exts = "psb",
    .id32 = "PSB\0", //24b/masked IDs?
    .id32 = get_id32be("PSB\0"), //???
    .idfn = psb_check_id,
}
#endif