/*
 * Copyright (C) 2001 Quake done Quick
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 */
// movie_avi.c

#include "quakedef.h"
#include "movie_avi.h"
#include <windows.h>
#include <vfw.h>

static void   (CALLBACK * qAVIFileInit) (void);
static        HRESULT (CALLBACK * qAVIFileOpen) (PAVIFILE *, LPCTSTR, UINT, LPCLSID);
static        HRESULT (CALLBACK * qAVIFileCreateStream) (PAVIFILE, PAVISTREAM *, AVISTREAMINFO *);
static        HRESULT (CALLBACK * qAVIMakeCompressedStream) (PAVISTREAM *, PAVISTREAM, AVICOMPRESSOPTIONS *, CLSID *);
static        HRESULT (CALLBACK * qAVIStreamSetFormat) (PAVISTREAM, LONG, LPVOID, LONG);
static        HRESULT (CALLBACK * qAVIStreamWrite) (PAVISTREAM, LONG, LONG, LPVOID, LONG, DWORD, LONG *, LONG *);
static        ULONG (CALLBACK * qAVIStreamRelease) (PAVISTREAM);
static        ULONG (CALLBACK * qAVIFileRelease) (PAVIFILE);
static void   (CALLBACK * qAVIFileExit) (void);

static        MMRESULT (ACMAPI * qacmDriverOpen) (LPHACMDRIVER, HACMDRIVERID, DWORD);
static        MMRESULT (ACMAPI * qacmDriverDetails) (HACMDRIVERID, LPACMDRIVERDETAILS, DWORD);
static        MMRESULT (ACMAPI * qacmDriverEnum) (ACMDRIVERENUMCB, DWORD, DWORD);
static        MMRESULT (ACMAPI * qacmFormatTagDetails) (HACMDRIVER, LPACMFORMATTAGDETAILS, DWORD);
static        MMRESULT (ACMAPI * qacmStreamOpen) (LPHACMSTREAM, HACMDRIVER, LPWAVEFORMATEX, LPWAVEFORMATEX,
                                                  LPWAVEFILTER, DWORD, DWORD, DWORD);
static        MMRESULT (ACMAPI * qacmStreamSize) (HACMSTREAM, DWORD, LPDWORD, DWORD);
static        MMRESULT (ACMAPI * qacmStreamPrepareHeader) (HACMSTREAM, LPACMSTREAMHEADER, DWORD);
static        MMRESULT (ACMAPI * qacmStreamUnprepareHeader) (HACMSTREAM, LPACMSTREAMHEADER, DWORD);
static        MMRESULT (ACMAPI * qacmStreamConvert) (HACMSTREAM, LPACMSTREAMHEADER, DWORD);
static        MMRESULT (ACMAPI * qacmStreamClose) (HACMSTREAM, DWORD);
static        MMRESULT (ACMAPI * qacmDriverClose) (HACMDRIVER, DWORD);

static HINSTANCE avi_handle = NULL, acm_handle = NULL;

PAVIFILE      m_file;
PAVISTREAM    m_uncompressed_video_stream, m_compressed_video_stream, m_audio_stream;

unsigned long m_codec_fourcc;
int           m_video_frame_counter;
int           m_video_frame_size;

qboolean      m_audio_is_mp3;
int           m_audio_frame_counter;
WAVEFORMATEX  m_wave_format;
MPEGLAYER3WAVEFORMAT m_mp3_format;
qboolean      mp3_encoder_exists;
HACMDRIVER    m_mp3_driver;
HACMSTREAM    m_mp3_stream;
ACMSTREAMHEADER m_mp3_stream_header;

extern qboolean avi_loaded, acm_loaded;
extern cvar_t capture_codec, capture_fps, capture_mp3, capture_mp3_kbps;

#define AVI_GETFUNC(f) (qAVI##f = (void *)GetProcAddress(avi_handle, "AVI" #f))
#define ACM_GETFUNC(f) (qacm##f = (void *)GetProcAddress(acm_handle, "acm" #f))

void AVI_LoadLibrary (void) {
    avi_loaded = false;

    if (!(avi_handle = LoadLibrary ("avifil32.dll"))) {
        Con_Printf ("\x02" "Avi capturing module not found\n");
        goto fail;
    }

    AVI_GETFUNC (FileInit);
    AVI_GETFUNC (FileOpen);
    AVI_GETFUNC (FileCreateStream);
    AVI_GETFUNC (MakeCompressedStream);
    AVI_GETFUNC (StreamSetFormat);
    AVI_GETFUNC (StreamWrite);
    AVI_GETFUNC (StreamRelease);
    AVI_GETFUNC (FileRelease);
    AVI_GETFUNC (FileExit);

    avi_loaded = qAVIFileInit && qAVIFileOpen && qAVIFileCreateStream && qAVIMakeCompressedStream && qAVIStreamSetFormat
        && qAVIStreamWrite && qAVIStreamRelease && qAVIFileRelease && qAVIFileExit;

    if (!avi_loaded) {
        Con_Printf ("\x02" "Avi capturing module not initialized\n");
        goto fail;
    }

    Con_Printf ("Avi capturing module initialized\n");
    return;

  fail:
    if (avi_handle) {
        FreeLibrary (avi_handle);
        avi_handle = NULL;
    }
}

void ACM_LoadLibrary (void) {
    acm_loaded = false;

    if (!(acm_handle = LoadLibrary ("msacm32.dll"))) {
        Con_Printf ("\x02" "ACM module not found\n");
        goto fail;
    }

    ACM_GETFUNC (DriverOpen);
    ACM_GETFUNC (DriverEnum);
    ACM_GETFUNC (StreamOpen);
    ACM_GETFUNC (StreamSize);
    ACM_GETFUNC (StreamPrepareHeader);
    ACM_GETFUNC (StreamUnprepareHeader);
    ACM_GETFUNC (StreamConvert);
    ACM_GETFUNC (StreamClose);
    ACM_GETFUNC (DriverClose);
    qacmDriverDetails = (void *) GetProcAddress (acm_handle, "acmDriverDetailsA");
    qacmFormatTagDetails = (void *) GetProcAddress (acm_handle, "acmFormatTagDetailsA");

    acm_loaded = qacmDriverOpen && qacmDriverDetails && qacmDriverEnum && qacmFormatTagDetails && qacmStreamOpen
        && qacmStreamSize && qacmStreamPrepareHeader && qacmStreamUnprepareHeader && qacmStreamConvert
        && qacmStreamClose && qacmDriverClose;

    if (!acm_loaded) {
        Con_Printf ("\x02" "ACM module not initialized\n");
        goto fail;
    }

    Con_Printf ("ACM module initialized\n");
    return;

  fail:
    if (acm_handle) {
        FreeLibrary (acm_handle);
        acm_handle = NULL;
    }
}

PAVISTREAM Capture_VideoStream (void) {
    return m_codec_fourcc ? m_compressed_video_stream : m_uncompressed_video_stream;
}

BOOL CALLBACK acmDriverEnumCallback (HACMDRIVERID mp3_driver_id, DWORD dwInstance, DWORD fdwSupport) {
    if (fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CODEC) {
        int           i;
        ACMDRIVERDETAILS drvDetails;

        memset (&drvDetails, 0, sizeof (drvDetails));
        drvDetails.cbStruct = sizeof (drvDetails);
        qacmDriverDetails (mp3_driver_id, &drvDetails, 0);
        qacmDriverOpen (&m_mp3_driver, mp3_driver_id, 0);

        for (i = 0; i < drvDetails.cFormatTags; i++) {
            ACMFORMATTAGDETAILS fmtDetails;

            memset (&fmtDetails, 0, sizeof (fmtDetails));
            fmtDetails.cbStruct = sizeof (fmtDetails);
            fmtDetails.dwFormatTagIndex = i;
            qacmFormatTagDetails (m_mp3_driver, &fmtDetails, ACM_FORMATTAGDETAILSF_INDEX);
            if (fmtDetails.dwFormatTag == WAVE_FORMAT_MPEGLAYER3) {
                MMRESULT      mmr;

                Con_DPrintf ("MP3-capable ACM codec found: %s\n", drvDetails.szLongName);

                m_mp3_stream = NULL;
                if ((mmr =
                     qacmStreamOpen (&m_mp3_stream, m_mp3_driver, &m_wave_format, &m_mp3_format.wfx, NULL, 0, 0, 0))) {
                    switch (mmr) {
                        case MMSYSERR_INVALPARAM:
                            Con_DPrintf ("ERROR: Invalid parameters passed to acmStreamOpen\n");
                            break;

                        case ACMERR_NOTPOSSIBLE:
                            Con_DPrintf ("ERROR: No ACM filter found capable of encoding MP3\n");
                            break;

                        default:
                            Con_DPrintf ("ERROR: Couldn't open ACM encoding stream\n");
                            break;
                    }
                    continue;
                }
                mp3_encoder_exists = true;

                return false;
            }
        }

        qacmDriverClose (m_mp3_driver, 0);
    }

    return true;
}

qboolean Capture_Open (char *filename) {
    HRESULT       hr;
    BITMAPINFOHEADER bitmap_info_header;
    AVISTREAMINFO stream_header;
    char         *fourcc;

    m_video_frame_counter = m_audio_frame_counter = 0;
    m_file = NULL;
    m_codec_fourcc = 0;
    m_uncompressed_video_stream = m_compressed_video_stream = m_audio_stream = NULL;
    m_audio_is_mp3 = (qboolean) capture_mp3.value;

    if (*(fourcc = capture_codec.string) != '0')        // codec fourcc
        // supplied
        m_codec_fourcc = mmioFOURCC (fourcc[0], fourcc[1], fourcc[2], fourcc[3]);

    qAVIFileInit ();
    hr = qAVIFileOpen (&m_file, filename, OF_WRITE | OF_CREATE, NULL);
    if (FAILED (hr)) {
        Con_Printf ("ERROR: Couldn't open AVI file\n");
        return false;
    }
    // initialize video data
#ifdef GLQUAKE
    m_video_frame_size = glwidth * glheight * 3;
#else
    m_video_frame_size = vid.width * vid.height * 3;
#endif

    memset (&bitmap_info_header, 0, sizeof (bitmap_info_header));
    bitmap_info_header.biSize = sizeof (BITMAPINFOHEADER);
#ifdef GLQUAKE
    bitmap_info_header.biWidth = glwidth;
    bitmap_info_header.biHeight = glheight;
#else
    bitmap_info_header.biWidth = vid.width;
    bitmap_info_header.biHeight = vid.height;
#endif
    bitmap_info_header.biPlanes = 1;
    bitmap_info_header.biBitCount = 24;
    bitmap_info_header.biCompression = BI_RGB;
    bitmap_info_header.biSizeImage = m_video_frame_size;

    memset (&stream_header, 0, sizeof (stream_header));
    stream_header.fccType = streamtypeVIDEO;
    stream_header.fccHandler = m_codec_fourcc;
    stream_header.dwScale = 1;
    stream_header.dwRate = (unsigned long) (0.5 + capture_fps.value);
    stream_header.dwSuggestedBufferSize = bitmap_info_header.biSizeImage;
    SetRect (&stream_header.rcFrame, 0, 0, bitmap_info_header.biWidth, bitmap_info_header.biHeight);

    hr = qAVIFileCreateStream (m_file, &m_uncompressed_video_stream, &stream_header);
    if (FAILED (hr)) {
        Con_Printf ("ERROR: Couldn't create video stream\n");
        return false;
    }

    if (m_codec_fourcc) {
        AVICOMPRESSOPTIONS opts;

        memset (&opts, 0, sizeof (opts));
        opts.fccType = stream_header.fccType;
        opts.fccHandler = m_codec_fourcc;

        // Make the stream according to compression
        hr = qAVIMakeCompressedStream (&m_compressed_video_stream, m_uncompressed_video_stream, &opts, NULL);
        if (FAILED (hr)) {
            Con_Printf ("ERROR: Couldn't make compressed video stream\n");
            return false;
        }
    }

    hr = qAVIStreamSetFormat (Capture_VideoStream (), 0, &bitmap_info_header, bitmap_info_header.biSize);
    if (FAILED (hr)) {
        Con_Printf ("ERROR: Couldn't set video stream format\n");
        return false;
    }
    // initialize audio data
    memset (&m_wave_format, 0, sizeof (m_wave_format));
    m_wave_format.wFormatTag = WAVE_FORMAT_PCM;
    m_wave_format.nChannels = 2;       // always stereo in Quake sound engine
    m_wave_format.nSamplesPerSec = shm->speed;
    m_wave_format.wBitsPerSample = 16; // always 16bit in Quake sound engine
    m_wave_format.nBlockAlign = m_wave_format.wBitsPerSample / 8 * m_wave_format.nChannels;
    m_wave_format.nAvgBytesPerSec = m_wave_format.nSamplesPerSec * m_wave_format.nBlockAlign;

    memset (&stream_header, 0, sizeof (stream_header));
    stream_header.fccType = streamtypeAUDIO;
    stream_header.dwScale = m_wave_format.nBlockAlign;
    stream_header.dwRate = stream_header.dwScale * (unsigned long) m_wave_format.nSamplesPerSec;
    stream_header.dwSampleSize = m_wave_format.nBlockAlign;

    hr = qAVIFileCreateStream (m_file, &m_audio_stream, &stream_header);
    if (FAILED (hr)) {
        Con_Printf ("ERROR: Couldn't create audio stream\n");
        return false;
    }

    if (m_audio_is_mp3) {
        memset (&m_mp3_format, 0, sizeof (m_mp3_format));
        m_mp3_format.wfx.wFormatTag = WAVE_FORMAT_MPEGLAYER3;
        m_mp3_format.wfx.nChannels = 2;
        m_mp3_format.wfx.nSamplesPerSec = shm->speed;
        m_mp3_format.wfx.wBitsPerSample = 0;
        m_mp3_format.wfx.nBlockAlign = 1;
        m_mp3_format.wfx.nAvgBytesPerSec = capture_mp3_kbps.value * 125;
        m_mp3_format.wfx.cbSize = MPEGLAYER3_WFX_EXTRA_BYTES;
        m_mp3_format.wID = MPEGLAYER3_ID_MPEG;
        m_mp3_format.fdwFlags = MPEGLAYER3_FLAG_PADDING_OFF;
        m_mp3_format.nBlockSize = m_mp3_format.wfx.nAvgBytesPerSec / capture_fps.value;
        m_mp3_format.nFramesPerBlock = 1;
        m_mp3_format.nCodecDelay = 1393;

        // try to find an MP3 codec
        m_mp3_driver = NULL;
        mp3_encoder_exists = false;
        qacmDriverEnum (acmDriverEnumCallback, 0, 0);
        if (!mp3_encoder_exists) {
            Con_Printf ("ERROR: Couldn't find any MP3 encoder\n");
            return false;
        }

        hr = qAVIStreamSetFormat (m_audio_stream, 0, &m_mp3_format, sizeof (MPEGLAYER3WAVEFORMAT));
        if (FAILED (hr)) {
            Con_Printf ("ERROR: Couldn't set audio stream format\n");
            return false;
        }
    } else {
        hr = qAVIStreamSetFormat (m_audio_stream, 0, &m_wave_format, sizeof (WAVEFORMATEX));
        if (FAILED (hr)) {
            Con_Printf ("ERROR: Couldn't set audio stream format\n");
            return false;
        }
    }

    return true;
}

void Capture_Close (void) {
    if (m_uncompressed_video_stream)
        qAVIStreamRelease (m_uncompressed_video_stream);
    if (m_compressed_video_stream)
        qAVIStreamRelease (m_compressed_video_stream);
    if (m_audio_stream)
        qAVIStreamRelease (m_audio_stream);
    if (m_audio_is_mp3) {
        qacmStreamClose (m_mp3_stream, 0);
        qacmDriverClose (m_mp3_driver, 0);
    }
    if (m_file)
        qAVIFileRelease (m_file);

    qAVIFileExit ();
}

void Capture_WriteVideo (byte * pixel_buffer) {
    HRESULT       hr;
#ifdef GLQUAKE
    int           size = glwidth * glheight * 3;
#else
    int           size = vid.width * vid.height * 3;
#endif

    if (m_video_frame_size != size) {
        Con_Printf ("ERROR: Frame size changed\n");
        return;
    }

    if (!Capture_VideoStream ()) {
        Con_Printf ("ERROR: Video stream is NULL\n");
        return;
    }

    hr = qAVIStreamWrite (Capture_VideoStream (), m_video_frame_counter++, 1, pixel_buffer, m_video_frame_size,
                          AVIIF_KEYFRAME, NULL, NULL);
    if (FAILED (hr)) {
        Con_Printf ("ERROR: Couldn't write video stream\n");
        return;
    }
}

void Capture_WriteAudio (int samples, byte * sample_buffer) {
    HRESULT       hr;
    unsigned long sample_bufsize;

    if (!m_audio_stream) {
        Con_Printf ("ERROR: Audio stream is NULL\n");
        return;
    }

    sample_bufsize = samples * m_wave_format.nBlockAlign;
    if (m_audio_is_mp3) {
        MMRESULT      mmr;
        byte         *mp3_buffer;
        unsigned long mp3_bufsize;

        if ((mmr = qacmStreamSize (m_mp3_stream, sample_bufsize, &mp3_bufsize, ACM_STREAMSIZEF_SOURCE))) {
            Con_Printf ("ERROR: Couldn't get mp3bufsize\n");
            return;
        }
        if (!mp3_bufsize) {
            Con_Printf ("ERROR: mp3bufsize is zero\n");
            return;
        }
        mp3_buffer = Q_calloc (mp3_bufsize, 1);

        memset (&m_mp3_stream_header, 0, sizeof (m_mp3_stream_header));
        m_mp3_stream_header.cbStruct = sizeof (m_mp3_stream_header);
        m_mp3_stream_header.pbSrc = sample_buffer;
        m_mp3_stream_header.cbSrcLength = sample_bufsize;
        m_mp3_stream_header.pbDst = mp3_buffer;
        m_mp3_stream_header.cbDstLength = mp3_bufsize;

        if ((mmr = qacmStreamPrepareHeader (m_mp3_stream, &m_mp3_stream_header, 0))) {
            Con_Printf ("ERROR: Couldn't prepare header\n");
            Q_free (mp3_buffer);
            return;
        }

        if ((mmr = qacmStreamConvert (m_mp3_stream, &m_mp3_stream_header, ACM_STREAMCONVERTF_BLOCKALIGN))) {
            Con_Printf ("ERROR: Couldn't convert audio stream\n");
            goto clean;
        }

        hr = qAVIStreamWrite (m_audio_stream, m_audio_frame_counter++, 1, mp3_buffer,
                              m_mp3_stream_header.cbDstLengthUsed, AVIIF_KEYFRAME, NULL, NULL);

      clean:
        if ((mmr = qacmStreamUnprepareHeader (m_mp3_stream, &m_mp3_stream_header, 0))) {
            Con_Printf ("ERROR: Couldn't unprepare header\n");
            Q_free (mp3_buffer);
            return;
        }

        Q_free (mp3_buffer);
    } else {
        hr = qAVIStreamWrite (m_audio_stream, m_audio_frame_counter++, 1, sample_buffer, sample_bufsize, AVIIF_KEYFRAME,
                              NULL, NULL);
    }
    if (FAILED (hr)) {
        Con_Printf ("ERROR: Couldn't write audio stream\n");
        return;
    }
}
