#include <windows.h>
#include <stdio.h>

#include "kmp_pi.h"
#include "kbopus_decoder.h"
#include "../../../kpi_common/base64.h"

static void __fastcall SetPicture(const char* cszPicture, IKpiTagInfo *pTagInfo);
//IKpiFile 用
//////////////////////////////////////////////////////////////////////////////
static int read_func_stream(void *_stream, unsigned char *_ptr,int _nbytes)
{
    IKpiFile *pStream = (IKpiFile*)_stream;
    DWORD dwRet = pStream->Read(_ptr, _nbytes);
    return dwRet;
}
//////////////////////////////////////////////////////////////////////////////
static int seek_func_stream(void *_stream,opus_int64 _offset,int _whence)
{
    IKpiFile *pFile = (IKpiFile*)_stream;
    UINT64 ui64Ret = pFile->Seek(_offset, _whence);
    if(ui64Ret == KPI_FILE_EOF){
        return -1;
    }
    return 0;
}
//////////////////////////////////////////////////////////////////////////////
static ogg_int64_t tell_func_stream(void *_stream)
{
    IKpiFile *pFile = (IKpiFile*)_stream;
    return pFile->Seek(0, FILE_CURRENT);
}
//////////////////////////////////////////////////////////////////////////////
//KbOpusDecoder
//////////////////////////////////////////////////////////////////////////////
KbOpusDecoder::KbOpusDecoder(void)
{
    kpi_InitMediaInfo(&m_MediaInfo);
    m_pOpusFile = NULL;
    m_pFile = NULL;
    m_dwRemain = m_dwPointer = 0;
}
//////////////////////////////////////////////////////////////////////////////
KbOpusDecoder::~KbOpusDecoder(void)
{
    if(m_pOpusFile){
        op_free(m_pOpusFile);
    }
    if(m_pFile){
        m_pFile->Release();
    }
}
//////////////////////////////////////////////////////////////////////////////
DWORD __fastcall KbOpusDecoder::Open(const KPI_MEDIAINFO *cpRequesst, 
                                    IKpiFile *pFile, 
                                    IKpiFolder *pFolder)
{
    int op_error = 0;
    OpusTags *ot = NULL;
    OpusFileCallbacks callbacks;
    callbacks.read = read_func_stream;
    callbacks.seek = seek_func_stream;
    callbacks.close = NULL;
    callbacks.tell = tell_func_stream;
    pFile->Seek(0, FILE_BEGIN);

    m_pOpusFile = op_open_callbacks(pFile, &callbacks, NULL, 0, &op_error);
    if(!m_pOpusFile){
        return 0;
    }
    kpi_InitMediaInfo(&m_MediaInfo);
    
    ogg_int64_t totalSamples = op_pcm_total(m_pOpusFile, -1);

    m_MediaInfo.dwSampleRate = SAMPLE_RATE;
    m_MediaInfo.dwChannels = NCH;
    m_MediaInfo.nBitsPerSample = -32;
    m_MediaInfo.qwLength = kpi_SampleTo100ns(totalSamples, m_MediaInfo.dwSampleRate);
    m_MediaInfo.dwSeekableFlags = KPI_MEDIAINFO::SEEK_FLAGS_SAMPLE;
    m_MediaInfo.dwUnitSample = 0;
    m_MediaInfo.dwNumber = 1;
    m_MediaInfo.dwCount = 1;
    //リプレイゲインの処理は本体が行う
    op_set_gain_offset(m_pOpusFile, OP_ABSOLUTE_GAIN, 0);
    m_pFile = pFile;
    pFile->AddRef();
    return TRUE;
}
//////////////////////////////////////////////////////////////////////////////
DWORD WINAPI KbOpusDecoder::Select(DWORD dwNumber,
                                  const KPI_MEDIAINFO **ppMediaInfo,
                                  IKpiTagInfo *pTagInfo,
                                  DWORD dwTagGetFlags)
{
    if(ppMediaInfo){
        *ppMediaInfo = NULL;
    }
    if (dwNumber > m_MediaInfo.dwCount) {
        return 0;
    }
    GetTagInfo(pTagInfo, dwTagGetFlags);
    if(!ppMediaInfo){//タグ取得のみ(タイトル/アーティストのみ)
        return dwNumber;
    }
    *ppMediaInfo = &m_MediaInfo;
    return dwNumber;
}
//////////////////////////////////////////////////////////////////////////////
UINT64 WINAPI KbOpusDecoder::Seek(UINT64 qwPosSample, DWORD dwFlag)
{
    ogg_int64_t offset = OP_EINVAL;
    int ret = op_pcm_seek(m_pOpusFile, qwPosSample);
    if(ret == 0) {
	    offset = op_pcm_tell(m_pOpusFile);
    } 
    if(offset == OP_EINVAL){
        ret = op_pcm_seek(m_pOpusFile, 0);
        if(ret == 0) {
	        offset = op_pcm_tell(m_pOpusFile);
        } 
    }
    m_dwRemain = m_dwPointer = 0;
    return offset;
}
//////////////////////////////////////////////////////////////////////////////
DWORD WINAPI KbOpusDecoder::Render(BYTE *pBuffer, DWORD dwSizeSample)
{
    DWORD dwRetSample = 0;
    DWORD dwCopySample = m_dwRemain;
    if(dwCopySample){
        if(dwCopySample > dwSizeSample){
            dwCopySample = dwSizeSample;
        }
        memcpy(pBuffer, m_DecodedBuffer + m_dwPointer * BLOCK_ALIGN, dwCopySample * BLOCK_ALIGN);
        m_dwRemain -= dwCopySample;
        m_dwPointer += dwCopySample;
        dwRetSample += dwCopySample;
    }
    while(dwRetSample < dwSizeSample){
        int ret = op_read_float_stereo(m_pOpusFile, (float*)m_DecodedBuffer, OPUS_BLOCKSIZE);
        if(ret <= 0){//デコード終了
            break;
        }
        dwCopySample = ret;
        if(dwRetSample + dwCopySample > dwSizeSample){
            dwCopySample = dwSizeSample - dwRetSample;
        }
        memcpy(pBuffer + dwRetSample * BLOCK_ALIGN, m_DecodedBuffer, dwCopySample * BLOCK_ALIGN);
        dwRetSample += dwCopySample;
        m_dwRemain = ret - dwCopySample;
        m_dwPointer = dwCopySample;
    }
    return dwRetSample;
}
//////////////////////////////////////////////////////////////////////////////
void __fastcall KbOpusDecoder::GetTagInfo(IKpiTagInfo *pTagInfo, DWORD dwTagGetFlags)
{
    if(!pTagInfo){//NULL が渡されることがある
        return;
    }
    static const struct _S
    {
        const char    *cszField;
        const wchar_t *cszName;
        DWORD dwFlag;
    }_s[] = {
        {"title=",  L"title" , KPI_TAGGET_FLAG_TITLE},
        {"artist=", L"artist" , KPI_TAGGET_FLAG_ARTIST},
        {"album=",  L"album" , KPI_TAGGET_FLAG_ALBUM},
        {"albumartist=",  L"albumartist" , KPI_TAGGET_FLAG_ALBMART},
        {"copyright=",  L"copyright" , KPI_TAGGET_FLAG_COPYRIGHT},
        {"genre=",  L"genre" , KPI_TAGGET_FLAG_GENRE},
        {"comment=",  L"comment" , KPI_TAGGET_FLAG_COMMENT},
        {"description=",  L"description" , KPI_TAGGET_FLAG_COMMENT},
        {"tracknumber=",  L"tracknumber" , KPI_TAGGET_FLAG_TRACK},
        {"tracktotal=",  L"tracktotal" , KPI_TAGGET_FLAG_TRACK},
        {"discnumber=",  L"discnumber" , KPI_TAGGET_FLAG_TRACK},
        {"disctotal=",  L"disctotal" , KPI_TAGGET_FLAG_TRACK},
        {"replaygain_track_gain=",  L"replaygain_track_gain" , KPI_TAGGET_FLAG_RG},
        {"replaygain_track_peak=",  L"replaygain_track_peak" , KPI_TAGGET_FLAG_RG},
        {"replaygain_album_gain=",  L"replaygain_album_gain" , KPI_TAGGET_FLAG_RG},
        {"replaygain_album_peak=",  L"replaygain_album_peak" , KPI_TAGGET_FLAG_RG},
        {"METADATA_BLOCK_PICTURE=",  L"METADATA_BLOCK_PICTURE" , KPI_TAGGET_FLAG_PICTURE},
        {NULL, NULL, 0}
    };
	const OpusTags *pTags = op_tags(m_pOpusFile, -1);
	const OpusHead *pHead = (dwTagGetFlags & KPI_TAGGET_FLAG_GENERAL) ? op_head(m_pOpusFile, -1) : NULL;
    if(pHead){
        wchar_t szSampleRate[256];
        _snwprintf_s(szSampleRate, _TRUNCATE, L"%d", pHead->input_sample_rate);
        pTagInfo->wSetValueW(L"input_sample_rate", -1, szSampleRate, -1);
    }
    if(!pTags) {
		return;
	}
    int i;
    BOOL bTitleFound = FALSE;
    BOOL bArtistFound = FALSE;
    for(i = 0; i < pTags->comments; i++){
        bool bNotify = false;            
        char *pszComment = pTags->user_comments[i];
        const _S *s = _s;
        while(s->cszField){
            const int field_len = strlen(s->cszField);
            if(_strnicmp(pszComment, s->cszField, field_len) == 0 &&
               pszComment[field_len-1] == '='){
                if((s->dwFlag & dwTagGetFlags)){
                    const char *cszContentsU8 = pszComment + field_len;
                    if(s->dwFlag & KPI_TAGGET_FLAG_PICTURE){
                        SetPicture(cszContentsU8, pTagInfo);
                    }
                    else{
                        pTagInfo->wSetValueU8(s->cszName, -1, cszContentsU8, -1);
                    }
                    bNotify = true;
                }
                break;
            }
            s++;
        }
        if(bNotify){//通知済み
            continue;
        }
        if(!(dwTagGetFlags & KPI_TAGGET_FLAG_GENERAL)){//一般的でないタグは取得不要
            continue;
        }
        int j = 0;
        while(pszComment[j] != '='){
            if(pszComment[j] == 0)break;
            j++;
        }
        if(pszComment[j] == 0){
            continue;
        }
        int nNameLen = j;
        int nValueLen = pTags->comment_lengths[i] - j - 1;
        pTagInfo->aSetValueU8(pszComment, nNameLen, 
                           &pszComment[j+1], nValueLen);
    }
    if(!(dwTagGetFlags & KPI_TAGGET_FLAG_GENERAL)){
        return;
    }
    if(pTags->vendor){
        pTagInfo->wSetValueU8(L"vendor", -1, pTags->vendor, -1);
    }
    int bitrate = op_bitrate(m_pOpusFile, -1);
    wchar_t szValue[512];
    if(bitrate > 1000){
        _snwprintf_s(szValue, _TRUNCATE, L"%dkbps", bitrate / 1000);
    }
    else{
        _snwprintf_s(szValue, _TRUNCATE, L"%dbps", bitrate);
    }
    pTagInfo->wSetValueW(L"bitrate", -1, szValue, -1);
    //output_gain + album_gain = アルバムゲイン(album_gain=0?)
    //output_gain + track_gain = トラックゲイン
    int output_gain = pHead->output_gain;
    int track_gain = 0;
    int album_gain = 0;
    opus_tags_get_track_gain(pTags, &track_gain);
    opus_tags_get_album_gain(pTags, &album_gain);
    track_gain += output_gain;
    album_gain += output_gain;

    //replaygain_xxx に変換するには +5dB する必要がある?
    double dTrackGain = 5.0 + track_gain / 256.0;
    if(dTrackGain != 0.0){
        _snwprintf_s(szValue, _TRUNCATE, L"%+2.2f dB", dTrackGain);
        pTagInfo->wSetValueW(SZ_KMP_NAME_REPLAYGAIN_TRACKGAIN_W, -1, szValue, -1);
    }
    double dAlbumGain = 5.0 + album_gain / 256.0;
    if(dAlbumGain != 0.0){
        _snwprintf_s(szValue, _TRUNCATE, L"%+2.2f dB", dAlbumGain);
        pTagInfo->wSetValueW(SZ_KMP_NAME_REPLAYGAIN_ALBUMGAIN_W, -1, szValue, -1);
    }
}
//////////////////////////////////////////////////////////////////////////////
static void __fastcall Utf8ToUtf16(const char *cszUtf8,
                              wchar_t **ppszUtf16,
                              int nSrcSize)
{// BOMなしUTF8 を UTF16 に変換
    // UTF8 => Unicode
    int nLenUtf16 = MultiByteToWideChar(CP_UTF8, 0, cszUtf8, nSrcSize, NULL, 0);
    wchar_t *pszUtf16;
    pszUtf16 = (wchar_t*)malloc((nLenUtf16 + 1)*sizeof(wchar_t));
    MultiByteToWideChar(CP_UTF8, 0, cszUtf8, nSrcSize, pszUtf16, nLenUtf16);
    pszUtf16[nLenUtf16] = 0;
    *ppszUtf16 = pszUtf16;
}
//////////////////////////////////////////////////////////////////////////////
static inline DWORD bswap32(DWORD x)
{
    return (x << 24) | ((x << 8) & 0x00FF0000) | ((x >> 8) & 0x0000FF00) | (x >> 24);
}
//////////////////////////////////////////////////////////////////////////////
static void __fastcall SetPicture(const char* cszPicture, IKpiTagInfo *pTagInfo)
{
//int __fastcall b64Decode(const wchar_t *cszSrc, BYTE **ppDest, DWORD *pdwSize)
    wchar_t *pszPictureW = NULL;
    BYTE *pData = NULL;
    DWORD dwSize = 0;
    Utf8ToUtf16(cszPicture, &pszPictureW, -1);
    b64Decode(pszPictureW, &pData, &dwSize);
    free(pszPictureW);
    if(!pData){
        return;
    }
    /*
    struct METDATA_BLOCK_PICTURE{
        DWORD dwType;       //4
        DWORD dwSizeMIME;   //4
        char  szMIME[];     //
        DWORD dwSizeDesc;   //4
        char  szDesc[]; 
        DWORD width;        //4
        DWORD height;       //4
        DWORD colordepth;   //4
        DWORD indexed_color;//4
        DWORD dwPictureSize;//4
        BYTE  PictureData[];
        //TotalSize=32バイト以上(szMIME/szDesc/PictureDataを除く)
    */
    if(dwSize < 32){
        free(pData);
        return;
    }
    BYTE *p = pData;
    DWORD dwType = bswap32(*(DWORD*)p);     p += sizeof(DWORD);
    DWORD dwSizeMIME = bswap32(*(DWORD*)p); p += sizeof(DWORD);
    if(dwSize < 32 + dwSizeMIME){
        free(pData);
        return;
    }
    char  *pszMIME = (char*)p;     p += dwSizeMIME;
    DWORD dwSizeDesc = bswap32(*(DWORD*)p); p += sizeof(DWORD);
    if(dwSize < 32 + dwSizeMIME + dwSizeDesc){
        free(pData);
        return;
    }
    char  *pszDesc = (char*)p; p += dwSizeDesc;
    DWORD width = bswap32(*(DWORD*)p);  p += sizeof(DWORD);
    DWORD height = bswap32(*(DWORD*)p); p += sizeof(DWORD);
    DWORD color_depth = bswap32(*(DWORD*)p); p += sizeof(DWORD);
    DWORD indexed_color = bswap32(*(DWORD*)p); p += sizeof(DWORD);
    DWORD dwPictureSize = bswap32(*(DWORD*)p); p += sizeof(DWORD);
    if(p + dwPictureSize != pData + dwSize){
        free(pData);
        return;
    }
    BYTE *pPictureData = p;
    wchar_t *pszMIMEW;
    wchar_t *pszDescW;
    Utf8ToUtf16(pszMIME, &pszMIMEW, dwSizeMIME);//正確には pszMIME は Ascii だがどっちでも良い
    Utf8ToUtf16(pszDesc, &pszDescW, dwSizeDesc);
    pTagInfo->SetPicture(dwType, L"", pszMIMEW, pszDescW, width, height, pPictureData, dwPictureSize);
    free(pszDescW);
    free(pszMIMEW);
    free(pData);
}
