#include <ctype.h>
#include "vgmstream.h"
#include "coding/coding.h"
#include "mixing.h"


/*******************************************************************************/
/* TEXT                                                                        */
/*******************************************************************************/

static void describe_get_time(int32_t samples, int sample_rate, double* p_time_mm, double* p_time_ss) {
    double seconds = (double)samples / sample_rate;
    *p_time_mm = (int)(seconds / 60.0);
    *p_time_ss = seconds - *p_time_mm * 60.0f;
    if (*p_time_ss >= 59.999) /* avoid round up to 60.0 when printing to %06.3f */
        *p_time_ss = 59.999;
}

/* Write a description of the stream into array pointed by desc, which must be length bytes long.
 * Will always be null-terminated if length > 0 */
void describe_vgmstream(VGMSTREAM* vgmstream, char* desc, int length) {
#define TEMPSIZE (256+32)
    char temp[TEMPSIZE];
    double time_mm, time_ss;

    desc[0] = '\0';

    if (!vgmstream) {
        snprintf(temp,TEMPSIZE, "NULL VGMSTREAM");
        concatn(length,desc,temp);
        return;
    }

    snprintf(temp,TEMPSIZE, "sample rate: %d Hz\n", vgmstream->sample_rate);
    concatn(length,desc,temp);

    snprintf(temp,TEMPSIZE, "channels: %d\n", vgmstream->channels);
    concatn(length,desc,temp);

    {
        int output_channels = 0;
        mixing_info(vgmstream, NULL, &output_channels);

        if (output_channels != vgmstream->channels) {
            snprintf(temp,TEMPSIZE, "input channels: %d\n", vgmstream->channels); /* repeated but mainly for plugins */
            concatn(length,desc,temp);
            snprintf(temp,TEMPSIZE, "output channels: %d\n", output_channels);
            concatn(length,desc,temp);
        }
    }

    if (vgmstream->channel_layout) {
        int cl = vgmstream->channel_layout;

        /* not "channel layout: " to avoid mixups with "layout: " */
        snprintf(temp,TEMPSIZE, "channel mask: 0x%x /", vgmstream->channel_layout);
        concatn(length,desc,temp);
        if (cl & speaker_FL)    concatn(length,desc," FL");
        if (cl & speaker_FR)    concatn(length,desc," FR");
        if (cl & speaker_FC)    concatn(length,desc," FC");
        if (cl & speaker_LFE)   concatn(length,desc," LFE");
        if (cl & speaker_BL)    concatn(length,desc," BL");
        if (cl & speaker_BR)    concatn(length,desc," BR");
        if (cl & speaker_FLC)   concatn(length,desc," FLC");
        if (cl & speaker_FRC)   concatn(length,desc," FRC");
        if (cl & speaker_BC)    concatn(length,desc," BC");
        if (cl & speaker_SL)    concatn(length,desc," SL");
        if (cl & speaker_SR)    concatn(length,desc," SR");
        if (cl & speaker_TC)    concatn(length,desc," TC");
        if (cl & speaker_TFL)   concatn(length,desc," TFL");
        if (cl & speaker_TFC)   concatn(length,desc," TFC");
        if (cl & speaker_TFR)   concatn(length,desc," TFR");
        if (cl & speaker_TBL)   concatn(length,desc," TBL");
        if (cl & speaker_TBC)   concatn(length,desc," TBC");
        if (cl & speaker_TBR)   concatn(length,desc," TBR");
        concatn(length,desc,"\n");
    }

    /* times mod sounds avoid round up to 60.0 */
    if (vgmstream->loop_start_sample >= 0 && vgmstream->loop_end_sample > vgmstream->loop_start_sample) {
        if (!vgmstream->loop_flag) {
            concatn(length,desc,"looping: disabled\n");
        }

        describe_get_time(vgmstream->loop_start_sample, vgmstream->sample_rate, &time_mm, &time_ss);
        snprintf(temp,TEMPSIZE, "loop start: %d samples (%1.0f:%06.3f seconds)\n", vgmstream->loop_start_sample, time_mm, time_ss);
        concatn(length,desc,temp);

        describe_get_time(vgmstream->loop_end_sample, vgmstream->sample_rate, &time_mm, &time_ss);
        snprintf(temp,TEMPSIZE, "loop end: %d samples (%1.0f:%06.3f seconds)\n", vgmstream->loop_end_sample, time_mm, time_ss);
        concatn(length,desc,temp);
    }

    describe_get_time(vgmstream->num_samples, vgmstream->sample_rate, &time_mm, &time_ss);
    snprintf(temp,TEMPSIZE, "stream total samples: %d (%1.0f:%06.3f seconds)\n", vgmstream->num_samples, time_mm, time_ss);
    concatn(length,desc,temp);

    snprintf(temp,TEMPSIZE, "encoding: ");
    concatn(length,desc,temp);
    get_vgmstream_coding_description(vgmstream, temp, TEMPSIZE);
    concatn(length,desc,temp);
    concatn(length,desc,"\n");

    snprintf(temp,TEMPSIZE, "layout: ");
    concatn(length,desc,temp);
    get_vgmstream_layout_description(vgmstream, temp, TEMPSIZE);
    concatn(length, desc, temp);
    concatn(length,desc,"\n");

    if (vgmstream->layout_type == layout_interleave && vgmstream->channels > 1) {
        snprintf(temp,TEMPSIZE, "interleave: %#x bytes\n", (int32_t)vgmstream->interleave_block_size);
        concatn(length,desc,temp);

        if (vgmstream->interleave_first_block_size && vgmstream->interleave_first_block_size != vgmstream->interleave_block_size) {
            snprintf(temp,TEMPSIZE, "interleave first block: %#x bytes\n", (int32_t)vgmstream->interleave_first_block_size);
            concatn(length,desc,temp);
        }

        if (vgmstream->interleave_last_block_size && vgmstream->interleave_last_block_size != vgmstream->interleave_block_size) {
            snprintf(temp,TEMPSIZE, "interleave last block: %#x bytes\n", (int32_t)vgmstream->interleave_last_block_size);
            concatn(length,desc,temp);
        }
    }

    /* codecs with configurable frame size */
    if (vgmstream->frame_size > 0 || vgmstream->interleave_block_size > 0) {
        int32_t frame_size = vgmstream->frame_size > 0 ? vgmstream->frame_size : vgmstream->interleave_block_size;
        switch (vgmstream->coding_type) {
            case coding_MSADPCM:
            case coding_MSADPCM_int:
            case coding_MSADPCM_ck:
            case coding_MS_IMA:
            case coding_MC3:
            case coding_WWISE_IMA:
            case coding_REF_IMA:
            case coding_PSX_cfg:
                snprintf(temp,TEMPSIZE, "frame size: %#x bytes\n", frame_size);
                concatn(length,desc,temp);
                break;
            default:
                break;
        }
    }

    snprintf(temp,TEMPSIZE, "metadata from: ");
    concatn(length,desc,temp);
    get_vgmstream_meta_description(vgmstream, temp, TEMPSIZE);
    concatn(length,desc,temp);
    concatn(length,desc,"\n");

    snprintf(temp,TEMPSIZE, "bitrate: %d kbps\n", get_vgmstream_average_bitrate(vgmstream) / 1000);
    concatn(length,desc,temp);

    /* only interesting if more than one */
    if (vgmstream->num_streams > 1) {
        snprintf(temp,TEMPSIZE, "stream count: %d\n", vgmstream->num_streams);
        concatn(length,desc,temp);
    }

    if (vgmstream->num_streams > 1) {
        snprintf(temp,TEMPSIZE, "stream index: %d\n", vgmstream->stream_index == 0 ? 1 : vgmstream->stream_index);
        concatn(length,desc,temp);
    }

    if (vgmstream->stream_name[0] != '\0') {
        snprintf(temp,TEMPSIZE, "stream name: %s\n", vgmstream->stream_name);
        concatn(length,desc,temp);
    }

    if (vgmstream->config_enabled) {
        int32_t samples = vgmstream->pstate.play_duration;

        describe_get_time(samples, vgmstream->sample_rate, &time_mm, &time_ss);
        snprintf(temp,TEMPSIZE, "play duration: %d samples (%1.0f:%06.3f seconds)\n", samples, time_mm, time_ss);
        concatn(length,desc,temp);
    }
}

void describe_vgmstream_info(VGMSTREAM* vgmstream, vgmstream_info* info) {
    if (!info) {
        return;
    }

    memset(info, 0, sizeof(*info));

    if (!vgmstream) {
        return;
    }

    info->sample_rate = vgmstream->sample_rate;

    info->channels = vgmstream->channels;

    {
        int output_channels = 0;
        mixing_info(vgmstream, NULL, &output_channels);

        if (output_channels != vgmstream->channels) {
            info->mixing_info.input_channels = vgmstream->channels;
            info->mixing_info.output_channels = output_channels;
        }
    }

    info->channel_layout = vgmstream->channel_layout;

    if (vgmstream->loop_start_sample >= 0 && vgmstream->loop_end_sample > vgmstream->loop_start_sample) {
        info->loop_info.start = vgmstream->loop_start_sample;
        info->loop_info.end = vgmstream->loop_end_sample;
    }

    info->num_samples = vgmstream->num_samples;

    get_vgmstream_coding_description(vgmstream, info->encoding, sizeof(info->encoding));

    get_vgmstream_layout_description(vgmstream, info->layout, sizeof(info->layout));

    if (vgmstream->layout_type == layout_interleave && vgmstream->channels > 1) {
        info->interleave_info.value = vgmstream->interleave_block_size;

        if (vgmstream->interleave_first_block_size && vgmstream->interleave_first_block_size != vgmstream->interleave_block_size) {
            info->interleave_info.first_block = vgmstream->interleave_first_block_size;
        }

        if (vgmstream->interleave_last_block_size && vgmstream->interleave_last_block_size != vgmstream->interleave_block_size) {
            info->interleave_info.last_block = vgmstream->interleave_last_block_size;
        }
    }

    /* codecs with configurable frame size */
    if (vgmstream->frame_size > 0 || vgmstream->interleave_block_size > 0) {
        int32_t frame_size = vgmstream->frame_size > 0 ? vgmstream->frame_size : vgmstream->interleave_block_size;
        switch (vgmstream->coding_type) {
        case coding_MSADPCM:
        case coding_MSADPCM_int:
        case coding_MSADPCM_ck:
        case coding_MS_IMA:
        case coding_MC3:
        case coding_WWISE_IMA:
        case coding_REF_IMA:
        case coding_PSX_cfg:
            info->frame_size = frame_size;
            break;
        default:
            break;
        }
    }

    get_vgmstream_meta_description(vgmstream, info->metadata, sizeof(info->metadata));

    info->bitrate = get_vgmstream_average_bitrate(vgmstream);

    /* only interesting if more than one */
    if (vgmstream->num_streams > 1) {
        info->stream_info.total = vgmstream->num_streams;
    }
    else {
        info->stream_info.total = 1;
    }

    if (vgmstream->num_streams > 1) {
        info->stream_info.current = vgmstream->stream_index == 0 ? 1 : vgmstream->stream_index;
    }

    if (vgmstream->stream_name[0] != '\0') {
        snprintf(info->stream_info.name, sizeof(info->stream_info.name), "%s", vgmstream->stream_name);
    }
}


/*******************************************************************************/
/* BITRATE                                                                     */
/*******************************************************************************/

#define BITRATE_FILES_MAX 128 /* arbitrary max, but +100 segments have been observed */
typedef struct {
    uint32_t hash[BITRATE_FILES_MAX]; /* already used streamfiles */
    int subsong[BITRATE_FILES_MAX]; /* subsongs of those streamfiles (could be incorporated to the hash?) */
    int count;
    int count_max;
} bitrate_info_t;

static uint32_t hash_sf(STREAMFILE* sf) {
    int i;
    char path[PATH_LIMIT];
    uint32_t hash = 2166136261;

    get_streamfile_name(sf, path, sizeof(path));

    /* our favorite garbo hash a.k.a FNV-1 32b */
    i = 0;
    while (path[i] != '\0') {
        char c = tolower(path[i]);
        hash = (hash * 16777619) ^ (uint8_t)c;
        i++;
    }

    return hash;
}

/* average bitrate helper to get STREAMFILE for a channel, since some codecs may use their own */
static STREAMFILE* get_vgmstream_average_bitrate_channel_streamfile(VGMSTREAM* vgmstream, int channel) {

    if (vgmstream->coding_type == coding_NWA) {
        return nwa_get_streamfile(vgmstream->codec_data);
    }

    if (vgmstream->coding_type == coding_ACM) {
        return acm_get_streamfile(vgmstream->codec_data);
    }

    if (vgmstream->coding_type == coding_COMPRESSWAVE) {
        return compresswave_get_streamfile(vgmstream->codec_data);
    }

#ifdef VGM_USE_VORBIS
    if (vgmstream->coding_type == coding_OGG_VORBIS) {
        return ogg_vorbis_get_streamfile(vgmstream->codec_data);
    }
#endif
    if (vgmstream->coding_type == coding_CRI_HCA) {
        return hca_get_streamfile(vgmstream->codec_data);
    }
#ifdef VGM_USE_FFMPEG
    if (vgmstream->coding_type == coding_FFmpeg) {
        return ffmpeg_get_streamfile(vgmstream->codec_data);
    }
#endif
#if defined(VGM_USE_MP4V2) && defined(VGM_USE_FDKAAC)
    if (vgmstream->coding_type == coding_MP4_AAC) {
        mp4_aac_codec_data *data = vgmstream->codec_data;
        return data ? data->if_file.streamfile : NULL;
    }
#endif

    return vgmstream->ch[channel].streamfile;
}

static int get_vgmstream_file_bitrate_from_size(size_t size, int sample_rate, int32_t length_samples) {
    if (sample_rate == 0 || length_samples == 0) return 0;
    if (length_samples < 100) return 0; /* ignore stupid bitrates caused by some segments */
    return (int)((int64_t)size * 8 * sample_rate / length_samples);
}
static int get_vgmstream_file_bitrate_from_streamfile(STREAMFILE* sf, int sample_rate, int32_t length_samples) {
    if (sf == NULL) return 0;
    return get_vgmstream_file_bitrate_from_size(get_streamfile_size(sf), sample_rate, length_samples);
}

static int get_vgmstream_file_bitrate_main(VGMSTREAM* vgmstream, bitrate_info_t* br, int* p_uniques) {
    int i, ch;
    int bitrate = 0;

    /* Recursively get bitrate and fill the list of streamfiles if needed (to filter),
     * since layouts can include further vgmstreams that may also share streamfiles.
     *
     * Because of how data, layers and segments can be combined it's possible to
     * fool this in various ways; metas should report stream_size in complex cases
     * to get accurate bitrates (particularly for subsongs). An edge case is when
     * segments use only a few samples from a full file (like Wwise transitions), bitrates
     * become a bit high since its hard to detect only part of the file is needed. */

    if (vgmstream->stream_size != 0) {
        /* format may report full size for custom layouts that otherwise get odd values */
        bitrate += get_vgmstream_file_bitrate_from_size(vgmstream->stream_size, vgmstream->sample_rate, vgmstream->num_samples);
        if (p_uniques)
            (*p_uniques)++;
    }
    else if (vgmstream->layout_type == layout_segmented) {
        int uniques = 0;
        segmented_layout_data *data = (segmented_layout_data *) vgmstream->layout_data;
        for (i = 0; i < data->segment_count; i++) {
            bitrate += get_vgmstream_file_bitrate_main(data->segments[i], br, &uniques);
        }
        if (uniques)
            bitrate /= uniques; /* average */
    }
    else if (vgmstream->layout_type == layout_layered) {
        layered_layout_data *data = vgmstream->layout_data;
        for (i = 0; i < data->layer_count; i++) {
            bitrate += get_vgmstream_file_bitrate_main(data->layers[i], br, NULL);
        }
    }
    else {
        /* Add channel bitrate if streamfile hasn't been used before, so bitrate doesn't count repeats
         * (like same STREAMFILE reopened per channel, also considering SFs may be wrapped). */
        for (ch = 0; ch < vgmstream->channels; ch++) {
            uint32_t hash_cur;
            int subsong_cur;
            STREAMFILE* sf_cur;
            int is_unique = 1; /* default to "no other SFs exist" */

            /* compares paths (hashes for faster compares) + subsongs (same file + different subsong = "different" file) */
            sf_cur = get_vgmstream_average_bitrate_channel_streamfile(vgmstream, ch);
            if (!sf_cur) continue;

            hash_cur = hash_sf(sf_cur);
            subsong_cur = vgmstream->stream_index;

            for (i = 0; i < br->count; i++) {
                uint32_t hash_cmp = br->hash[i];
                int subsong_cmp = br->subsong[i];

                if (hash_cur == hash_cmp && subsong_cur == subsong_cmp) {
                    is_unique = 0;
                    break;
                }
            }

            if (is_unique) {
                size_t file_bitrate;

                if (br->count >= br->count_max) goto fail;
                
                if (vgmstream->stream_size) {
                    /* stream_size applies to both channels but should add once and detect repeats (for current subsong) */
                    file_bitrate = get_vgmstream_file_bitrate_from_size(vgmstream->stream_size, vgmstream->sample_rate, vgmstream->num_samples);
                }
                else {
                    file_bitrate = get_vgmstream_file_bitrate_from_streamfile(sf_cur, vgmstream->sample_rate, vgmstream->num_samples);
                }

                /* possible in cases like using silence codec */
                if (!file_bitrate)
                    break;

                br->hash[br->count] = hash_cur;
                br->subsong[br->count] = subsong_cur;

                br->count++;
                if (p_uniques)
                    (*p_uniques)++;

                bitrate += file_bitrate;

                break;
            }
        }
    }

    return bitrate;
fail:
    return 0;
}

/* Return the average bitrate in bps of all unique data contained within this stream.
 * This is the bitrate of the *file*, as opposed to the bitrate of the *codec*, meaning
 * it counts extra data like block headers and padding. While this can be surprising
 * sometimes (as it's often higher than common codec bitrates) it isn't wrong per se. */
int get_vgmstream_average_bitrate(VGMSTREAM* vgmstream) {
    bitrate_info_t br = {0};
    br.count_max = BITRATE_FILES_MAX;

    return get_vgmstream_file_bitrate_main(vgmstream, &br, NULL);
}
