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


/* ************************************************************************************************
 * FSB defines, copied from the public spec (https://www.fmod.org/questions/question/forum-4928/)
 * for reference. The format is mostly compatible for FSB1/2/3/4, but not FSB5.
 * ************************************************************************************************ */
/* These flags are used for FMOD_FSB_HEADER::mode */
#define FMOD_FSB_SOURCE_FORMAT         0x00000001  /* all samples stored in their original compressed format */
#define FMOD_FSB_SOURCE_BASICHEADERS   0x00000002  /* samples should use the basic header structure */
#define FMOD_FSB_SOURCE_ENCRYPTED      0x00000004  /* all sample data is encrypted */
#define FMOD_FSB_SOURCE_BIGENDIANPCM   0x00000008  /* pcm samples have been written out in big-endian format */
#define FMOD_FSB_SOURCE_NOTINTERLEAVED 0x00000010  /* Sample data is not interleaved. */
#define FMOD_FSB_SOURCE_MPEG_PADDED    0x00000020  /* Mpeg frames are now rounded up to the nearest 2 bytes for normal sounds, or 16 bytes for multichannel. */
#define FMOD_FSB_SOURCE_MPEG_PADDED4   0x00000040  /* Mpeg frames are now rounded up to the nearest 4 bytes for normal sounds, or 16 bytes for multichannel. */

/* These flags are used for FMOD_FSB_HEADER::version */
#define FMOD_FSB_VERSION_3_0         0x00030000  /* FSB version 3.0 */
#define FMOD_FSB_VERSION_3_1         0x00030001  /* FSB version 3.1 */
#define FMOD_FSB_VERSION_4_0         0x00040000  /* FSB version 4.0 */

/* FMOD 3 defines.  These flags are used for FMOD_FSB_SAMPLE_HEADER::mode */
#define FSOUND_LOOP_OFF              0x00000001  /* For non looping samples. */
#define FSOUND_LOOP_NORMAL           0x00000002  /* For forward looping samples. */
#define FSOUND_LOOP_BIDI             0x00000004  /* For bidirectional looping samples.  (no effect if in hardware). */
#define FSOUND_8BITS                 0x00000008  /* For 8 bit samples. */
#define FSOUND_16BITS                0x00000010  /* For 16 bit samples. */
#define FSOUND_MONO                  0x00000020  /* For mono samples. */
#define FSOUND_STEREO                0x00000040  /* For stereo samples. */
#define FSOUND_UNSIGNED              0x00000080  /* For user created source data containing unsigned samples. */
#define FSOUND_SIGNED                0x00000100  /* For user created source data containing signed data. */
#define FSOUND_MPEG                  0x00000200  /* For MPEG layer 2/3 data. */
#define FSOUND_CHANNELMODE_ALLMONO   0x00000400  /* Sample is a collection of mono channels. */
#define FSOUND_CHANNELMODE_ALLSTEREO 0x00000800  /* Sample is a collection of stereo channel pairs */
#define FSOUND_HW3D                  0x00001000  /* Attempts to make samples use 3d hardware acceleration. (if the card supports it) */
#define FSOUND_2D                    0x00002000  /* Tells software (not hardware) based sample not to be included in 3d processing. */
#define FSOUND_SYNCPOINTS_NONAMES    0x00004000  /* Specifies that syncpoints are present with no names */
#define FSOUND_DUPLICATE             0x00008000  /* This subsound is a duplicate of the previous one i.e. it uses the same sample data but w/different mode bits */
#define FSOUND_CHANNELMODE_PROTOOLS  0x00010000  /* Sample is 6ch and uses L C R LS RS LFE standard. */
#define FSOUND_MPEGACCURATE          0x00020000  /* For FSOUND_Stream_Open - for accurate FSOUND_Stream_GetLengthMs/FSOUND_Stream_SetTime.  WARNING, see FSOUND_Stream_Open for inital opening time performance issues. */
#define FSOUND_HW2D                  0x00080000  /* 2D hardware sounds.  allows hardware specific effects */
#define FSOUND_3D                    0x00100000  /* 3D software sounds */
#define FSOUND_32BITS                0x00200000  /* For 32 bit (float) samples. */
#define FSOUND_IMAADPCM              0x00400000  /* Contents are stored compressed as IMA ADPCM */
#define FSOUND_VAG                   0x00800000  /* For PS2 only - Contents are compressed as Sony VAG format */
#define FSOUND_XMA                   0x01000000  /* For Xbox360 only - Contents are compressed as XMA format */
#define FSOUND_GCADPCM               0x02000000  /* For Gamecube only - Contents are compressed as Gamecube DSP-ADPCM format */
#define FSOUND_MULTICHANNEL          0x04000000  /* For PS2 and Gamecube only - Contents are interleaved into a multi-channel (more than stereo) format */
#define FSOUND_OGG                   0x08000000  /* For vorbis encoded ogg data */
#define FSOUND_CELT                  0x08000000  /* For vorbis encoded ogg data */
#define FSOUND_MPEG_LAYER3           0x10000000  /* Data is in MP3 format. */
#define FSOUND_MPEG_LAYER2           0x00040000  /* Data is in MP2 format. */
#define FSOUND_LOADMEMORYIOP         0x20000000  /* For PS2 only - "name" will be interpreted as a pointer to data for streaming and samples.  The address provided will be an IOP address */
#define FSOUND_IMAADPCMSTEREO        0x20000000  /* Signify IMA ADPCM is actually stereo not two interleaved mono */
#define FSOUND_IGNORETAGS            0x40000000  /* Skips id3v2 etc tag checks when opening a stream, to reduce seek/read overhead when opening files (helps with CD performance) */
#define FSOUND_SYNCPOINTS            0x80000000  /* Specifies that syncpoints are present */

/* These flags are used for FMOD_FSB_SAMPLE_HEADER::mode */
#define FSOUND_CHANNELMODE_MASK      (FSOUND_CHANNELMODE_ALLMONO | FSOUND_CHANNELMODE_ALLSTEREO | FSOUND_CHANNELMODE_PROTOOLS)
#define FSOUND_CHANNELMODE_DEFAULT   0x00000000  /* Determine channel assignment automatically from channel count. */
#define FSOUND_CHANNELMODE_RESERVED  0x00000C00
#define FSOUND_NORMAL                (FSOUND_16BITS | FSOUND_SIGNED | FSOUND_MONO)
#define FSB_SAMPLE_DATA_ALIGN        32


/* simplified struct based on the original definitions */
typedef enum { MPEG, IMA, PSX, XMA2, DSP, CELT, PCM8, PCM16 } fsb_codec_t;
typedef struct {
    /* main header */
    uint32_t id;
    int32_t  total_subsongs;
    uint32_t sample_headers_size; /* all of them including extended information */
    uint32_t data_size;
    uint32_t version; /* extended fsb version (in FSB 3/3.1/4) */
    uint32_t flags; /* flags common to all streams (in FSB 3/3.1/4)*/
    /* sample header */
    uint32_t num_samples;
    uint32_t stream_size;
    uint32_t loop_start;
    uint32_t loop_end;
    uint32_t mode;
    int32_t  sample_rate;
    uint16_t channels;
    /* extra */
    uint32_t base_header_size;
    uint32_t sample_header_min;
    off_t extradata_offset;
    off_t first_extradata_offset;

    meta_t meta_type;
    off_t name_offset;
    size_t name_size;

    int loop_flag;

    fsb_codec_t codec;
} fsb_header;

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

/* FSB1~4 - from games using FMOD audio middleware */
VGMSTREAM * init_vgmstream_fsb(STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;
    off_t start_offset;
    int target_subsong = streamFile->stream_index;
    fsb_header fsb = {0};


    /* checks
     * .fsb: standard
     * .bnk: Hard Corps Uprising (PS3) */
    if ( !check_extensions(streamFile, "fsb,bnk") )
        goto fail;

    /* check header */
    fsb.id = read_32bitBE(0x00,streamFile);
    if (fsb.id == 0x46534231) { /* "FSB1" (somewhat different from other fsbs) */
        fsb.meta_type = meta_FSB1;
        fsb.base_header_size = 0x10;
        fsb.sample_header_min = 0x40;

        /* main header */
        fsb.total_subsongs = read_32bitLE(0x04,streamFile);
        fsb.data_size = read_32bitLE(0x08,streamFile);
        fsb.sample_headers_size = 0x40;
        fsb.version = 0;
        fsb.flags = 0;

        if (fsb.total_subsongs > 1) goto fail;

        /* sample header (first stream only, not sure if there are multi-FSB1) */
        {
            off_t header_offset = fsb.base_header_size;

            fsb.name_offset = header_offset;
            fsb.name_size   = 0x20;
            fsb.num_samples = read_32bitLE(header_offset+0x20,streamFile);
            fsb.stream_size = read_32bitLE(header_offset+0x24,streamFile);
            fsb.sample_rate = read_32bitLE(header_offset+0x28,streamFile);
            /* 0x2c:?  0x2e:?  0x30:?  0x32:? */
            fsb.mode        = read_32bitLE(header_offset+0x34,streamFile);
            fsb.loop_start  = read_32bitLE(header_offset+0x38,streamFile);
            fsb.loop_end    = read_32bitLE(header_offset+0x3c,streamFile);

            fsb.channels = (fsb.mode & FSOUND_STEREO) ? 2 : 1;
            if (fsb.loop_end > fsb.num_samples) /* this seems common... */
                fsb.num_samples = fsb.loop_end;

            /* DSP coefs, seek tables, etc */
            fsb.extradata_offset = header_offset+fsb.sample_header_min;

            start_offset = fsb.base_header_size + fsb.sample_headers_size;
        }
    }
    else { /* other FSBs (common/extended format) */
        if (fsb.id == 0x46534232) { /* "FSB2" */
            fsb.meta_type = meta_FSB2;
            fsb.base_header_size  = 0x10;
            fsb.sample_header_min = 0x40; /* guessed */
        } else if (fsb.id == 0x46534233) { /* "FSB3" */
            fsb.meta_type = meta_FSB3;
            fsb.base_header_size  = 0x18;
            fsb.sample_header_min = 0x40;
        } else if (fsb.id == 0x46534234) { /* "FSB4" */
            fsb.meta_type = meta_FSB4;
            fsb.base_header_size  = 0x30;
            fsb.sample_header_min = 0x50;
        } else {
            goto fail;
        }

        /* main header */
        fsb.total_subsongs = read_32bitLE(0x04,streamFile);
        fsb.sample_headers_size = read_32bitLE(0x08,streamFile);
        fsb.data_size = read_32bitLE(0x0c,streamFile);
        if (fsb.base_header_size > 0x10) {
            fsb.version = read_32bitLE(0x10,streamFile);
            fsb.flags   = read_32bitLE(0x14,streamFile);
            /* FSB4: 0x18(8):hash  0x20(10):guid */
        } else {
            fsb.version = 0;
            fsb.flags   = 0;
        }

        if (fsb.version == FMOD_FSB_VERSION_3_1) {
            fsb.sample_header_min = 0x50;
        } else if (fsb.version != 0 /* FSB2 */
                && fsb.version != FMOD_FSB_VERSION_3_0
                && fsb.version != FMOD_FSB_VERSION_4_0) {
            goto fail;
        }

        if (fsb.sample_headers_size < fsb.sample_header_min) goto fail;
        if (target_subsong == 0) target_subsong = 1;
        if (target_subsong < 0 || target_subsong > fsb.total_subsongs || fsb.total_subsongs < 1) goto fail;

        /* sample header (N-stream) */
        {
            int i;
            off_t header_offset = fsb.base_header_size;
            off_t data_offset = fsb.base_header_size + fsb.sample_headers_size;

            /* find target_stream header (variable sized) */
            for (i = 0; i < fsb.total_subsongs; i++) {
                size_t stream_header_size;

                if ((fsb.flags & FMOD_FSB_SOURCE_BASICHEADERS) && i > 0) {
                    /* miniheader, all subsongs reuse first header [rare, ex. Biker Mice from Mars (PS2)] */
                    stream_header_size = 0x08;
                    fsb.num_samples = read_32bitLE(header_offset+0x00,streamFile);
                    fsb.stream_size = read_32bitLE(header_offset+0x04,streamFile);
                    fsb.loop_start = 0;
                    fsb.loop_end = 0;
                }
                else {
                    /* subsong header for normal files */
                    stream_header_size = (uint16_t)read_16bitLE(header_offset+0x00,streamFile);
                    fsb.name_offset = header_offset+0x02;
                    fsb.name_size   = 0x20-0x02;
                    fsb.num_samples = read_32bitLE(header_offset+0x20,streamFile);
                    fsb.stream_size = read_32bitLE(header_offset+0x24,streamFile);
                    fsb.loop_start  = read_32bitLE(header_offset+0x28,streamFile);
                    fsb.loop_end    = read_32bitLE(header_offset+0x2c,streamFile);
                    fsb.mode        = read_32bitLE(header_offset+0x30,streamFile);
                    fsb.sample_rate = read_32bitLE(header_offset+0x34,streamFile);
                    /* 0x38: defvol, 0x3a: defpan, 0x3c: defpri */
                    fsb.channels = read_16bitLE(header_offset+0x3e,streamFile);
                    /* FSB3.1/4:
                     *   0x40: mindistance, 0x44: maxdistance, 0x48: varfreq/size_32bits
                     *   0x4c: varvol, 0x4e: fsb.varpan */

                    /* DSP coefs, seek tables, etc */
                    if (stream_header_size > fsb.sample_header_min) {
                        fsb.extradata_offset = header_offset+fsb.sample_header_min;
                        if (fsb.first_extradata_offset == 0)
                            fsb.first_extradata_offset = fsb.extradata_offset;
                    }
                }

                if (i+1 == target_subsong) /* final data_offset found */
                    break;

                header_offset += stream_header_size;
                data_offset += fsb.stream_size; /* there is no offset so manually count */

                /* some subsongs offsets need padding (most FSOUND_IMAADPCM, few MPEG too [Hard Reset (PC) subsong 5])
                 * other codecs may set PADDED4 (ex. XMA) but don't seem to need it and work fine */
                if (fsb.flags & FMOD_FSB_SOURCE_MPEG_PADDED4) {
                    if (data_offset % 0x20)
                        data_offset += 0x20 - (data_offset % 0x20);
                }
            }
            if (i > fsb.total_subsongs)
                goto fail; /* not found */

            start_offset = data_offset;
        }
    }


    /* XOR encryption for some FSB4, though the flag is only seen after decrypting */
    //;VGM_ASSERT(fsb.flags & FMOD_FSB_SOURCE_ENCRYPTED, "FSB ENCRYPTED found\n");

    /* sometimes there is garbage at the end or missing bytes due to improper ripping */
    VGM_ASSERT(fsb.base_header_size + fsb.sample_headers_size + fsb.data_size != streamFile->get_size(streamFile),
               "FSB wrong head/data_size found (expected 0x%x vs 0x%x)\n",
               fsb.base_header_size + fsb.sample_headers_size + fsb.data_size, streamFile->get_size(streamFile));

    /* Loops unless disabled. FMOD default seems to be full loops (0/num_samples-1) without flags, for repeating tracks
     * that should loop and jingles/sfx that shouldn't. We'll try to disable looping if it looks jingly enough. */
    fsb.loop_flag = !(fsb.mode & FSOUND_LOOP_OFF);
    if(!(fsb.mode & FSOUND_LOOP_NORMAL)                             /* rarely set */
            && fsb.loop_start+fsb.loop_end+1 == fsb.num_samples     /* full loop */
            && fsb.num_samples < 20*fsb.sample_rate)                /* in seconds (lame but no other way to know) */
        fsb.loop_flag = 0;

    /* ping-pong looping = no looping? (forward > reverse > forward) [ex. Biker Mice from Mars (PS2)] */
    VGM_ASSERT(fsb.mode & FSOUND_LOOP_BIDI, "FSB BIDI looping found\n");

    /* convert to clean some code */
    if      (fsb.mode & FSOUND_MPEG)        fsb.codec = MPEG;
    else if (fsb.mode & FSOUND_IMAADPCM)    fsb.codec = IMA;
    else if (fsb.mode & FSOUND_VAG)         fsb.codec = PSX;
    else if (fsb.mode & FSOUND_XMA)         fsb.codec = XMA2;
    else if (fsb.mode & FSOUND_GCADPCM)     fsb.codec = DSP;
    else if (fsb.mode & FSOUND_CELT)        fsb.codec = CELT;
    else if (fsb.mode & FSOUND_8BITS)       fsb.codec = PCM8;
    else                                    fsb.codec = PCM16;


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

    vgmstream->sample_rate = fsb.sample_rate;
    vgmstream->num_samples = fsb.num_samples;
    vgmstream->loop_start_sample = fsb.loop_start;
    vgmstream->loop_end_sample = fsb.loop_end;
    vgmstream->num_streams = fsb.total_subsongs;
    vgmstream->stream_size = fsb.stream_size;
    vgmstream->meta_type = fsb.meta_type;
    if (fsb.name_offset)
        read_string(vgmstream->stream_name,fsb.name_size+1, fsb.name_offset,streamFile);

    switch(fsb.codec) {
#ifdef VGM_USE_MPEG
        case MPEG: { /* FSB4: Shatter (PS3), Way of the Samurai 3/4 (PS3) */
            mpeg_custom_config cfg = {0};

            cfg.fsb_padding = (vgmstream->channels > 2 ? 16 :
                (fsb.flags & FMOD_FSB_SOURCE_MPEG_PADDED4 ? 4 :
                (fsb.flags & FMOD_FSB_SOURCE_MPEG_PADDED ? 2 : 0)));

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

            //VGM_ASSERT(fsb.mode & FSOUND_MPEG_LAYER2, "FSB FSOUND_MPEG_LAYER2 found\n");/* not always set anyway */
            VGM_ASSERT(fsb.mode & FSOUND_IGNORETAGS, "FSB FSOUND_IGNORETAGS found\n"); /* not seen */
            break;
        }
#endif

        case IMA: /* FSB3: Bioshock (PC), FSB4: Blade Kitten (PC) */
            vgmstream->coding_type = coding_XBOX_IMA;
            vgmstream->layout_type = layout_none;
            /* "interleaved header" IMA, only used with >2ch (ex. Blade Kitten 6ch)
             * or (seemingly) when flag is used (ex. Dead to Rights 2 (Xbox) 2ch in FSB3.1) */
            if (vgmstream->channels > 2 || (fsb.mode & FSOUND_MULTICHANNEL))
                vgmstream->coding_type = coding_FSB_IMA;

            /* FSOUND_IMAADPCMSTEREO is "noninterleaved, true stereo IMA", but doesn't seem to be any different
             * (found in FSB4: Shatter, Blade Kitten (PC), Hard Corps: Uprising (PS3)) */
            break;

        case PSX:  /* FSB1: Jurassic Park Operation Genesis (PS2), FSB4: Spider Man Web of Shadows (PSP) */
            vgmstream->coding_type = coding_PSX;
            vgmstream->layout_type = layout_interleave;
            if (fsb.flags & FMOD_FSB_SOURCE_NOTINTERLEAVED) {
                vgmstream->interleave_block_size = fsb.stream_size / fsb.channels;
            }
            else {
                vgmstream->interleave_block_size = 0x10;
            }
            break;

#ifdef VGM_USE_FFMPEG
        case XMA2: { /* FSB3: The Bourne Conspiracy 2008 (X360), FSB4: Armored Core V (X360), Hard Corps (X360) */
            uint8_t buf[0x100];
            size_t bytes, block_size, block_count;

            block_size = 0x8000; /* FSB default */
            block_count = fsb.stream_size / block_size; /* not accurate but not needed (custom_data_offset+0x14 -1?) */

            bytes = ffmpeg_make_riff_xma2(buf,0x100, fsb.num_samples, fsb.stream_size, fsb.channels, fsb.sample_rate, block_count, block_size);
            vgmstream->codec_data = init_ffmpeg_header_offset(streamFile, buf,bytes, start_offset,fsb.stream_size);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;
            break;
        }
#endif

        case DSP: /* FSB3: Metroid Prime 3 (GC), FSB4: de Blob (Wii) */
            if (fsb.flags & FMOD_FSB_SOURCE_NOTINTERLEAVED) { /* [de Blob (Wii) sfx)] */
                vgmstream->coding_type = coding_NGC_DSP;
                vgmstream->layout_type = layout_interleave;
                vgmstream->interleave_block_size = fsb.stream_size / fsb.channels;
            }
            else {
                vgmstream->coding_type = coding_NGC_DSP_subint;
                vgmstream->layout_type = layout_none;
                vgmstream->interleave_block_size = 0x2;
            }
            dsp_read_coefs_be(vgmstream, streamFile, fsb.extradata_offset, 0x2e);
            break;

        case CELT: { /* FSB4: War Thunder (PC), The Witcher 2 (PC), Vessel (PC) */
            goto fail;

        case PCM8: /* assumed, no games known */
            vgmstream->coding_type = (fsb.mode & FSOUND_UNSIGNED) ? coding_PCM8_U : coding_PCM8;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x1;
            break;

        case PCM16: /* (PCM16) FSB4: Rocket Knight (PC), Another Century's Episode R (PS3), Toy Story 3 (Wii) */
            vgmstream->coding_type = (fsb.flags & FMOD_FSB_SOURCE_BIGENDIANPCM) ? coding_PCM16BE : coding_PCM16LE;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x2;

            /* sometimes FSOUND_MONO/FSOUND_STEREO is not set (ex. Dead Space iOS),
             * or only STEREO/MONO but not FSOUND_8BITS/FSOUND_16BITS is set */
            break;

        default:
            goto fail;
    }


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

fail:
    close_vgmstream(vgmstream);
    return NULL;
}

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

static STREAMFILE* setup_fsb4_wav_streamfile(STREAMFILE *streamfile, off_t subfile_offset, size_t subfile_size);

/* FSB4 with "\0WAV" Header, found in Deadly Creatures (Wii).
 * Has a 0x10 BE header that holds the filesize (unsure if this is from a proper rip). */
VGMSTREAM * init_vgmstream_fsb4_wav(STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;
    STREAMFILE *test_streamFile = NULL;
    off_t subfile_start = 0x10;
    size_t subfile_size = get_streamfile_size(streamFile) - 0x10 - 0x10;

    /* check extensions */
    if ( !check_extensions(streamFile, "fsb,wii") )
        goto fail;

    if (read_32bitBE(0x00,streamFile) != 0x00574156) /* "\0WAV" */
        goto fail;

    /* parse FSB subfile */
    test_streamFile = setup_fsb4_wav_streamfile(streamFile, subfile_start,subfile_size);
    if (!test_streamFile) goto fail;

    vgmstream = init_vgmstream_fsb(test_streamFile);
    if (!vgmstream) goto fail;

    /* init the VGMSTREAM */
    close_streamfile(test_streamFile);
    return vgmstream;

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

static STREAMFILE* setup_fsb4_wav_streamfile(STREAMFILE *streamFile, off_t subfile_offset, size_t subfile_size) {
    STREAMFILE *temp_streamFile = NULL, *new_streamFile = NULL;

    /* setup subfile */
    new_streamFile = open_wrap_streamfile(streamFile);
    if (!new_streamFile) goto fail;
    temp_streamFile = new_streamFile;

    new_streamFile = open_clamp_streamfile(temp_streamFile, subfile_offset,subfile_size);
    if (!new_streamFile) goto fail;
    temp_streamFile = new_streamFile;

    new_streamFile = open_fakename_streamfile(temp_streamFile, NULL,"fsb");
    if (!new_streamFile) goto fail;
    temp_streamFile = new_streamFile;

    return temp_streamFile;

fail:
    close_streamfile(temp_streamFile);
    return NULL;
}