/*
 * AviFile.cpp
 *
 *  Created on: 08/mar/2022
 *      Author: chessaa
 */

#include "AviFile.h"

#include <windows.h>
#include <mmsystem.h>
#include <vfw.h>

#include "gtl_cmdio.h"

#include <stdint.h>
//#include <atomic>

//Audio capture
#define BITS_PER_SAMPLE	8
#define BYTES_PER_SAMPLE	(BITS_PER_SAMPLE/8)

const int BUFFER_DURATION = 1;
const int CHANNEL_COUNT = 1;
const int SAMPLE_RATE = 22050; //48000; //44100;
const int DATA_SIZE = BYTES_PER_SAMPLE* SAMPLE_RATE * BUFFER_DURATION * CHANNEL_COUNT;
//const int BUFFER_SIZE = 10;
const int NUMBER_OF_BUFFERS = 8;

const unsigned int WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE = 0x0010;

AviFile* AviFile_instance;

#define AVI_LOG cmdio_debug

#ifdef CAPTURE_AUDIO
class AudioCaptureImplementation
{
public:
    void(*callback)(void* cookie, WAVEHDR data);
    void* callback_cookie;
    bool running;

    MMRESULT res;
    HWAVEIN micHandle;
    WAVEFORMATEX format;
    WAVEHDR buffers[32];
    unsigned int buffers_data[32][DATA_SIZE];

    volatile uint64_t buffers_queue;

    bool addBuffer(WAVEHDR *buffer);

    static void CALLBACK waveInProc(
       HWAVEIN   hwi,
       UINT      uMsg,
       DWORD_PTR dwInstance,
       DWORD_PTR dwParam1,
       DWORD_PTR dwParam2);

    void Update();

    bool StartCapture();
    void StopCapture();
    void closeCapture();

    CRITICAL_SECTION myCs;

    unsigned int time;

    AudioCaptureImplementation():
    	callback(0),
    	callback_cookie(0),
    	running(false),
    	res(0),
    	micHandle(0),
    	buffers_queue(0),
    	time(0)
    {
    	memset(&format, 0, sizeof format);
    	memset(buffers, 0, sizeof buffers);

    	InitializeCriticalSection(&myCs);
    }

    virtual ~AudioCaptureImplementation()
    {
    }

    void lock()
    {
    	EnterCriticalSection(&myCs);
    }
    void unlock()
    {
    	LeaveCriticalSection(&myCs);
    }

    int flush(bool disregard=false);
};

void CALLBACK AudioCaptureImplementation::waveInProc(
   HWAVEIN   hwi,
   UINT      uMsg,
   DWORD_PTR dwInstance,
   DWORD_PTR dwParam1,
   DWORD_PTR dwParam2)
{
	if (uMsg == WIM_DATA)
	{
		WAVEHDR* hdr = (WAVEHDR*)dwParam1;
		int id = hdr->dwUser;

		AudioCaptureImplementation& instance=*reinterpret_cast<AudioCaptureImplementation*>(dwInstance);


		instance.lock();
			instance.buffers_queue<<=8;
			instance.buffers_queue|=(id+1);
#if 0
			WAVEHDR& buffer=instance.buffers[id];

			//cmdio_psuccess("Audio Buffer: %d 0x%X %u samples", id, instance.buffers_queue, buffer.dwBytesRecorded);
#endif
		instance.unlock();

	}
}

bool AudioCaptureImplementation::addBuffer(WAVEHDR* buffer)
{
	res = waveInPrepareHeader(micHandle, buffer, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
	{
		cmdio_pfail("waveInPrepareHeader() failed %u", res);
		return false;
	}

	res = waveInAddBuffer(micHandle, buffer, sizeof(WAVEHDR));
	if (res != MMSYSERR_NOERROR)
	{
		cmdio_pfail("waveInAddBuffer() failed %u", res);
		return false;
	}
	return true;
}

bool AudioCaptureImplementation::StartCapture()
{

	if (micHandle==0)
	{
		memset(&format, 0, sizeof format);
		format.nChannels = CHANNEL_COUNT;
		format.cbSize = 0;
		format.nSamplesPerSec = SAMPLE_RATE;
		format.wFormatTag = WAVE_FORMAT_PCM;
		format.wBitsPerSample = BITS_PER_SAMPLE;
		format.nBlockAlign = (CHANNEL_COUNT * format.wBitsPerSample) / 8;
		format.nAvgBytesPerSec = SAMPLE_RATE * format.nBlockAlign;

		res=waveInOpen(&micHandle, WAVE_MAPPER, &format, (DWORD_PTR)waveInProc, (DWORD_PTR)this, CALLBACK_FUNCTION|WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE);
		if (res != MMSYSERR_NOERROR)
		{
			cmdio_perror("waveInOpen() failed");
			micHandle=0;
			return false;
		 }
		 for (int i = 0; i < NUMBER_OF_BUFFERS; i++)
			{
				WAVEHDR* header=&buffers[i]; //new WAVEHDR;
				ZeroMemory(header, sizeof(*header));


				header->lpData = (char*)buffers_data[i]; //(LPSTR)new short int[DATA_SIZE];
				header->dwBufferLength = DATA_SIZE;
				header->dwBytesRecorded = 0;
				header->dwUser = 0;
				header->dwFlags = 0;
				header->dwLoops = 0;
				header->dwUser = i;

				//buffers[i] = header;

				bool ok=addBuffer(&buffers[i]);
				if (ok)
					cmdio_printf(AVI_LOG, "Audio: adding buffer %d 0x%X", i, header->lpData);
			}
	 }
	 waveInStart(micHandle);
	 return true;
}

void AudioCaptureImplementation::StopCapture()
{
	if (micHandle)
		waveInStop(micHandle);
}

void AudioCaptureImplementation::closeCapture()
{
	if (micHandle)
	{
		waveInClose(micHandle);
	}
}

int AudioCaptureImplementation::flush(bool disregard)
{
	int one_sec=0;
	while(buffers_queue)
	{
		lock();
		unsigned int id=buffers_queue & 0x0FF;
		buffers_queue>>=8;
		unlock();

		if (id==0)
			continue;

		WAVEHDR& buffer=buffers[id-1];

		cmdio_printf(AVI_LOG, "Audio::flush(): %d 0x%p  %u", id, buffer.lpData, buffer.dwBytesRecorded);

	    if (!disregard)
	    {
	    	if (callback)
	    		callback(callback_cookie, buffer);
	    }
		addBuffer(&buffer);
		++one_sec;
	}
	return one_sec;
}
#else
class AudioCaptureImplementation
{
public:
    bool addBuffer(WAVEHDR */*buffer*/)
    {
    	return false;
    }

    void* callback_cookie;
    void(*callback)(void* cookie, WAVEHDR data);

    static void CALLBACK waveInProc(
       HWAVEIN   /*hwi*/,
       UINT      /*uMsg*/,
       DWORD_PTR /*dwInstance*/,
       DWORD_PTR /*dwParam1*/,
       DWORD_PTR /*dwParam2*/)
    {
    }

    void Update()
    {
    }

    bool StartCapture()
    {
    	return false;
    }

    void StopCapture()
    {
    }

    void closeCapture()
    {
    }


    void lock()
    {
    	//EnterCriticalSection(&myCs);
    }
    void unlock()
    {
    	//LeaveCriticalSection(&myCs);
    }

    int flush(bool /*disregard=false*/)
    {
    	return 0;
    }

};
#endif

class AviFile::Implementation
{
public:
	struct stream_t
	{
		bool ready;

		PAVISTREAM ps;
		PAVISTREAM psCompressed;
		PAVISTREAM psWrite;
		AVISTREAMINFO strhdr;
		AVICOMPRESSOPTIONS opts;
		BITMAPINFOHEADER bi;
	    WAVEFORMATEX format;

		unsigned long time;


		void close()
		{
			if (ready)
			{
				ready=false;
				if (ps)
					AVIStreamClose(ps);
				if (psCompressed)
					AVIStreamClose(psCompressed);
			}
		}
	};

	PAVIFILE avif;
	char fname[512];

	HRESULT hr;

	stream_t videoStream;
	stream_t audioStream;
	stream_t textStream;

	AudioCaptureImplementation audioCapturer;

	static DWORD myFOURCC()
	{
		//return mmioFOURCC('M', 'P', '4', 'V'); //('M', 'S', 'V', 'C');
		return mmioFOURCC('M','S','V','C');
	}

	Implementation():
		avif(0),
		hr(0)
	{
		memset(&videoStream, 0, sizeof videoStream);
		memset(&audioStream, 0, sizeof audioStream);
		memset(&textStream, 0, sizeof textStream);
	}

	bool createTextStrem_()
	{
		stream_t& s=textStream;
		memset(&s, 0, sizeof s);

		s.strhdr.fccType                = streamtypeTEXT;
		s.strhdr.fccHandler             = 0;
		s.strhdr.dwScale                = 1;
		s.strhdr.dwRate                 = 30;
		s.strhdr.dwLength               = 1;
		s.strhdr.dwQuality              = 0;
		strcpy(s.strhdr.szName, "SUBT");

        hr = AVIFileCreateStream(avif,             // file pointer
                                 &s.ps,                // returned stream pointer
                                 &s.strhdr);          // stream header

        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Video::AVIFileCreateStream(txts)");
            return false;
        }

		s.opts.fccType = streamtypeTEXT;
		s.opts.dwKeyFrameEvery = 1;
		s.opts.dwQuality=0;
		s.opts.dwFlags=0; //                = AVICOMPRESSF_KEYFRAMES|AVICOMPRESSF_VALID;
		s.opts.lpFormat=&s.format;
		s.opts.cbFormat=sizeof s.format;

        hr = AVIStreamSetFormat(s.ps, 0, &s.format, sizeof s.format);
        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Audio::AVIStreamSetFormat()");
        	return false;
        }

        s.ready=true;

        return true;
	}

	bool createVideoStream(int rate, int rectwidth, int rectheight, int key, int quality)
	{
		stream_t& s=videoStream;

		memset(&s, 0, sizeof s);

		if (!avif)
			return false;

		DWORD buffersize=rectwidth*rectheight*sizeof(uint32_t);

		s.strhdr.fccType                = streamtypeVIDEO;// stream type
		s.strhdr.fccHandler             = myFOURCC(); //mmioFOURCC('M','S','V','C'); // Microsoft video 1
		//strhdr.fccHandler             = mmioFOURCC('I','V','5','0'); // Intel video 5.0
		//strhdr.dwFlags                = AVISTREAMINFO_DISABLED;
		//strhdr.dwCaps                 =
		//strhdr.wPriority              =
		//strhdr.wLanguage              =
		s.strhdr.dwScale                = 10;
		s.strhdr.dwRate                 = rate;               // rate fps
		//strhdr.dwStart                =
		//strhdr.dwLength               =
		//strhdr.dwInitialFrames        =
		s.strhdr.dwSuggestedBufferSize  = buffersize;
		s.strhdr.dwQuality              = -1; // use the default
		strcpy(s.strhdr.szName, "video");

		//strhdr.dwSampleSize           =
        SetRect(&s.strhdr.rcFrame, 0, 0,              // rectangle for stream
            (int) rectwidth,
            (int) rectheight);

        hr = AVIFileCreateStream(avif,             // file pointer
                                 &s.ps,                // returned stream pointer
                                 &s.strhdr);          // stream header
        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Video::AVIFileCreateStream()");
            return false;
        }

		s.opts.fccType = streamtypeVIDEO;
		s.opts.fccHandler             = myFOURCC();
		//opts.fccHandler  = 0;
		//opts.fccHandler            = mmioFOURCC('D','I','B',' '); // Uncompressed
		//opts.fccHandler             = mmioFOURCC('C','V','I','D'); // Cinpak
		//opts.fccHandler             = mmioFOURCC('I','V','3','2'); // Intel video 3.2
		//opts.fccHandler             = mmioFOURCC('M','S','V','C'); // Microsoft video 1
		//opts.fccHandler             = mmioFOURCC('I','V','5','0'); // Intel video 5.0
		s.opts.dwKeyFrameEvery = key; //-1; //50;
		s.opts.dwQuality=quality; //-1;
		//opts.dwBytesPerSecond
		s.opts.dwFlags                = AVICOMPRESSF_KEYFRAMES|AVICOMPRESSF_VALID;
		//opts.lpFormat
		//opts.cbFormat
		//opts.lpParms
		//opts.cbParms
		//opts.dwInterleaveEvery
		/* display the compression options dialog box if specified compressor is unknown */

        hr = AVIMakeCompressedStream(&s.psCompressed, s.ps, &s.opts, 0);
        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Video::AVIMakeCompressedStream()");
        	return false;
        }

        s.bi.biHeight=rectheight;
        s.bi.biWidth=rectwidth;
        s.bi.biCompression=0;
        s.bi.biPlanes=1;
        s.bi.biBitCount=32;
        s.bi.biSizeImage=s.bi.biHeight*s.bi.biWidth*4;
        s.bi.biSize=sizeof s.bi;

        hr = AVIStreamSetFormat(s.psCompressed, 0,
        		&s.bi, s.bi.biSize + s.bi.biClrUsed * sizeof(RGBQUAD));

        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Video::AVIStreamSetFormat()");
        	return false;
        }

        s.ready=true;
        return true;
	}

	bool addVideoFrame(const void* data)
	{
		stream_t& s=videoStream;

		if (!s.ready)
		{
			cmdio_pwarning("addVideoFrame(): stream not ready");
			return false;
		}

		const DWORD ImageSize=s.bi.biSizeImage;

		hr = AVIStreamWrite(s.psCompressed, // stream pointer
			s.time, // time of this frame
			1, // number to write
			const_cast<void*>(data),
			ImageSize, // lpbi->biSizeImage, // size of this frame
			s.time==0 ? AVIIF_KEYFRAME : 0, // flags....
			NULL,
			NULL);
		++s.time;
		if (hr != AVIERR_OK)
		{
			//CString strMsg;
			//strMsg.Format("Error: AVIStreamWrite, error %d",hr);
			cmdio_oserror("addVideoFrame()=%d",hr);
			return false;
		}
		return true;
	}


	bool createAudioStream(int rate, int ch, int bytes_per_sample)
	{

		stream_t& s=audioStream;

		memset(&s, 0, sizeof s);

		if (!avif)
			return false;

		s.format.nChannels=ch;
		s.format.nSamplesPerSec=rate;
		s.format.wFormatTag=WAVE_FORMAT_PCM;
		s.format.wBitsPerSample=bytes_per_sample*8;
		s.format.nBlockAlign=(ch*s.format.wBitsPerSample)/8;
		s.format.nAvgBytesPerSec=ch*rate*s.format.nBlockAlign;

		s.strhdr.fccType                = streamtypeAUDIO;// stream type
		s.strhdr.dwScale                = 1;
		s.strhdr.dwRate                 = rate;               // rate fps
		//strhdr.dwStart                =
		//strhdr.dwLength               =
		//strhdr.dwInitialFrames        =
		s.strhdr.dwSuggestedBufferSize  = 0; //rate;
		s.strhdr.dwQuality              = -1; // use the default
		s.strhdr.rcFrame=videoStream.strhdr.rcFrame;

		strcpy(s.strhdr.szName, "audio");

        hr = AVIFileCreateStream(avif,             // file pointer
                                 &s.ps,                // returned stream pointer
                                 &s.strhdr);          // stream header
        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Audio::AVIFileCreateStream()");
            return false;
        }

        s.psWrite=s.ps;

		s.opts.fccType = streamtypeAUDIO;
		s.opts.dwKeyFrameEvery = 1;
		s.opts.dwQuality=-1;
		//opts.dwBytesPerSecond
		s.opts.dwFlags=0; //                = AVICOMPRESSF_KEYFRAMES|AVICOMPRESSF_VALID;
		s.opts.lpFormat=&s.format;
		s.opts.cbFormat=sizeof s.format;

		//opts.lpFormat
		//opts.cbFormat
		//opts.lpParms
		//opts.cbParms
		//opts.dwInterleaveEvery
		/* display the compression options dialog box if specified compressor is unknown */
#if 0
        hr = AVIMakeCompressedStream(&s.psCompressed, s.ps, &s.opts, 0);
        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Audio::AVIMakeCompressedStream()");
        	return false;
        }
        s.psWrite=s.psCompressed;
#endif

        hr = AVIStreamSetFormat(s.psWrite, 0, &s.format, sizeof s.format);
        if (hr != AVIERR_OK)
        {
        	cmdio_oserror("Audio::AVIStreamSetFormat()");
        	return false;
        }

        s.ready=true;
        audioCapturer.callback=audio_callback;
        audioCapturer.callback_cookie=this;
        return true;
	}

	bool addAudioSamples(int size, const void* data)
	{
		stream_t& s=audioStream;
		int numsmaples=size/(s.format.wBitsPerSample/8);
		if (!s.ready)
		{
			cmdio_pwarning("addAudioSamples(): stream not ready");
			return false;
		}

		if (!s.psWrite)
		{
			cmdio_pfail("addAudioSamples(): no write stream");
			return false;
		}

		hr = AVIStreamWrite(s.psWrite, // stream pointer
			s.time, // time of this frame
			numsmaples, // number to write
			const_cast<void*>(data),
			size, // lpbi->biSizeImage, // size of this frame
			0, 0, 0);

		s.time+=numsmaples;
		if (hr != AVIERR_OK)
		{
			//CString strMsg;
			//strMsg.Format("Error: AVIStreamWrite, error %d",hr);
			cmdio_oserror("addAudioSamples()=%d",hr);
			return false;
		}
		return true;
	}

	static void audio_callback(void* cookie, WAVEHDR hdr)
	{
	    cmdio_printf(AVI_LOG, "Audio::audio_callback(): %d", hdr.dwBytesRecorded);

		AviFile::Implementation* instance=reinterpret_cast<AviFile::Implementation*>(cookie);
		instance->addAudioSamples(hdr.dwBytesRecorded, hdr.lpData);
	}
};

AviFile::AviFile():
	p_(*new Implementation)
{
	AVIFileInit();
}

AviFile::~AviFile()
{
	close();
	delete &p_;
}

bool AviFile::open(const char* name, bool add_date)
{
	if (name)
	{
		strncpy(p_.fname, name, sizeof p_.fname);
	}
	else
		strcpy(p_.fname, "avi_file.avi");

	if (p_.avif)
		close();


    HRESULT hr = AVIFileOpen(&p_.avif,           // returned file pointer
                   p_.fname,                  // file name
                   OF_WRITE | OF_CREATE,      // mode to open file with
                   0);                     // use handler determined
                                              // from file extension....
    if (hr != AVIERR_OK)
    {
    	cmdio_oserror("AviFile::open(%s)", p_.fname);
        return false;
    }

	cmdio_psuccess("AviFile::open(%s)", p_.fname);
    return true;
}

bool AviFile::isOpen() const
{
	return p_.avif!=0;
}

bool AviFile::close()
{
	p_.audioCapturer.StopCapture();
	p_.videoStream.close();
	p_.audioStream.close();

	//addSubtitle(0, 0);

	if (p_.avif)
	{
		AVIFileClose(p_.avif);
		p_.avif=0;
		cmdio_psuccess("Video saved: %s", p_.fname);
	}
	return true;
}


bool AviFile::openVideoStream(int rate,int w, int h, int key, int quality)
{
	return p_.createVideoStream(rate, w, h,key, quality);
}

bool AviFile::addVideoFrame(const void* data)
{
	//if (!p_.textStream.ready) p_.createTextStrem();
	return p_.addVideoFrame(data);
}

bool AviFile::openAudioStream(int rate, int ch, int bytes_per_sample)
{
	return p_.createAudioStream(rate, ch, bytes_per_sample);
}
bool AviFile::openAudioStream()
{
	return p_.createAudioStream(SAMPLE_RATE, 1, BYTES_PER_SAMPLE);
}

bool AviFile::addAudioSamples(int samples, const void* data)
{
	return p_.addAudioSamples(samples, data);
}

bool AviFile::audioCapture(bool enable)
{
	if (enable)
		return p_.audioCapturer.StartCapture();
	else
	{
		p_.audioCapturer.StopCapture();
		return true;
	}
}

int AviFile::audioFlush(bool disregard)
{
	return p_.audioCapturer.flush(disregard);
}

AviFile* AviFile::instance()
{
	if (AviFile_instance==0)
	{
		AviFile_instance=new AviFile;
	}
	return AviFile_instance;
}


static void ascii2UTF16(char* dst, const char* src)
{
	for(int i=0; src[i]; ++i)
	{
		dst[i*2]=src[i];
		dst[i*2+1]=0;
	}
}

void AviFile::addSubtitle(const char* data, unsigned int size)
{
	return;
#if 0
	struct txt_header_t
	{
		unsigned char gab2[4];
		unsigned char byte0;
		unsigned short ucode02;
		unsigned int name_size;
		char name[32];
		unsigned short w04;
		unsigned int txt_size;

		char fake[2048];
	} __attribute__((packed));
	static txt_header_t txt;

	if (!p_.textStream.ready)
	{
		bool ok=p_.createTextStrem();
		if (!ok)
			return;
	}

	txt.gab2[0]='G'; txt.gab2[1]='A'; txt.gab2[2]='B'; txt.gab2[3]='2';
	txt.ucode02=2;
	txt.name_size=0;
	txt.txt_size=1024;
	txt.w04=0x04;
	txt.name_size=sizeof txt.name;
	ascii2UTF16(txt.name, "TXT");

	ascii2UTF16(txt.fake,
			"1\n"
			"00::00:10,000 --> 00:00:24,400\n"
			"prova prova!!!\n\n");

	LONG sampleWritten=0;
	LONG bytesWritten=0;
	AVIStreamWrite(p_.textStream.ps, 0, 1, &txt, sizeof txt, 0, &sampleWritten, &bytesWritten);

	cmdio_pinfo("txt %d %d", sampleWritten, bytesWritten);
	p_.textStream.close();
#endif
}

void AviFile::addSubtitleInstance(const char* data, unsigned int size)
{
	if (!AviFile_instance)
		return;

	AviFile_instance->addSubtitle(data, size);
}

