//	VirtualDub - Video processing and capture application
//	Copyright (C) 1998-2001 Avery Lee
//
//	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., 675 Mass Ave, Cambridge, MA 02139, USA.

#include <stdlib.h>
#include <crtdbg.h>
#include <math.h>

#include "VBitmap.h"
#include "Error.h"
#include "cpuaccel.h"

#include "VideoTelecineRemover.h"

class CVideoTelecineRemover: public VideoTelecineRemover {
public:
	CVideoTelecineRemover(VBitmap *pInFormat, bool, int, bool);
	~CVideoTelecineRemover();

	void ProcessIn(VBitmap *pIn, long);
	long ProcessOut(VBitmap *pOut);

private:
	char *	pMemBlock;
	long	nCombVals[10][2];
	long	lFrameNums[10];
	VBitmap	vb;
	int		nCurrentIn, nCurrentOut;
	int		nCombOffset1, nCombOffset2;
	int		nNewCombOffset1, nNewCombOffset2;
	int		nLag;
	bool	fInvertPolarity, fNewPolarity, fDropMode, fNewDropMode,
			fDecomb, fOffsetLocked;
};

VideoTelecineRemover *CreateVideoTelecineRemover(VBitmap *pInFormat, bool fDecomb, int iOffset, bool fInvertedPolarity) {
	return new CVideoTelecineRemover(pInFormat, fDecomb, iOffset, fInvertedPolarity);
}


CVideoTelecineRemover::CVideoTelecineRemover(VBitmap *pFormat, bool fDecomb, int iOffset, bool fInvertedPolarity) {
//	vb = *pFormat;

	vb = VBitmap(NULL, pFormat->w, pFormat->h, 24);

	if (!(pMemBlock = new char[vb.size * 10]))
		throw MyMemoryError();

	memset(pMemBlock, 0, vb.size * 10);

	if (fDecomb) {
		nCurrentIn = 0;
		nLag = 10;
	} else {
		nCurrentIn = 4;
		nLag = 6;
	}
	nCurrentOut = 9;

	if (iOffset<0) {
		nCombOffset1 = nNewCombOffset1 = -1;
		nCombOffset2 = nNewCombOffset2 = -1;
		fDropMode = fNewDropMode = true;
		fOffsetLocked = false;
	} else {
		fDropMode = false;
		nCombOffset1 = iOffset;
		nCombOffset2 = (iOffset+1) % 5;
		fInvertPolarity = fInvertedPolarity;
		fOffsetLocked = true;
	}
	this->fDecomb = fDecomb;

	memset(nCombVals, 0, sizeof nCombVals);
}

VideoTelecineRemover::~VideoTelecineRemover() {
}

CVideoTelecineRemover::~CVideoTelecineRemover() {
	delete[] pMemBlock;
}

static inline int sq(int d) {
	return d*d;
}

#if 0

static long __declspec(naked) computeScanImprovementMMX(Pixel8 *src1, Pixel8 *src2, PixOffset pitch, PixDim w) {
	__asm {
		push		ebx

		mov			eax,[esp+8]
		mov			ecx,[esp+16]
		mov			edx,[esp+12]
		mov			ebx,[esp+20]

		pxor		mm4,mm4
xloop:
		movd		mm0,[eax]
		pxor		mm7,mm7

		movd		mm1,[eax+ecx*2]
		punpcklbw	mm0,mm7

		movd		mm2,[eax+ecx]
		punpcklbw	mm1,mm7

		movd		mm3,[edx]
		punpcklbw	mm2,mm7			;mm2 = pB

		paddw		mm0,mm1			;mm0 = pA + pC
		paddw		mm2,mm2			;mm2 = 2*pB

		punpcklbw	mm3,mm7			;mm3 = pD
		psubw		mm2,mm0			;mm2 = 2*pB - (pA+pC)

		paddw		mm3,mm3			;mm3 = 2*pD
		pmaddwd		mm2,mm2			;mm2 = sq(2*pB - (pA+pC))

		psubw		mm3,mm0			;mm3 = 2*pD - (pA+pC)
		add			eax,4

		pmaddwd		mm3,mm3			;mm3 = sq(2*pD - (pA+pC))
		add			edx,4

		paddd		mm4,mm2
		dec			ebx

		;
		;

		psubd		mm4,mm3
		jne			xloop

		movd		eax,mm4
		psrlq		mm4,32
		movd		ecx,mm4
		add			eax,ecx

		pop			ebx
		ret
	}
}
#else
static long __declspec(naked) computeScanImprovementMMX(Pixel8 *src1, Pixel8 *src2, PixOffset pitch, PixDim w) {
	__asm {
		push		ebx

		mov			eax,[esp+8]
		mov			ecx,[esp+16]
		mov			edx,[esp+12]
		mov			ebx,[esp+20]

		pxor		mm6,mm6
xloop:
		movd		mm0,[eax]
		pxor		mm7,mm7

		movd		mm1,[eax+ecx*2]
		punpcklbw	mm0,mm7			;mm0 = pA

		movd		mm2,[eax+ecx]
		punpcklbw	mm1,mm7			;mm1 = pC

		movd		mm3,[edx]
		punpcklbw	mm2,mm7			;mm2 = pB

		movd		mm4,[edx+ecx*2]
		punpcklbw	mm3,mm7			;mm3 = pD

		movd		mm5,[edx+ecx]
		punpcklbw	mm4,mm7			;mm4 = pF

		punpcklbw	mm5,mm7			;mm5 = pE
		paddw		mm0,mm1			;mm0 = pA + pC

		paddw		mm3,mm4			;mm3 = pD + pF
		paddw		mm5,mm5			;mm5 = 2*pE

		paddw		mm2,mm2			;mm2 = 2*pB
		psubw		mm3,mm5			;mm3 = pD + pF - 2*pE

		psubw		mm5,mm0			;mm5 = 2*pE - (pA + pC)
		pmaddwd		mm3,mm3			;mm3 = sq(pD + pF - 2*pE)	[mm3 --- ---]

		psubw		mm0,mm2			;mm0 = pA + pC - 2*pB
		pmaddwd		mm5,mm5			;mm5 = sq(pA + pC - 2*pE)	[mm5 mm3 ---]

		pmaddwd		mm0,mm0			;mm0 = sq(pA + pC - 2*pB)	[mm0 mm5 mm3]
		add			eax,4

		paddd		mm6,mm3
		add			edx,4

		psubd		mm6,mm5
		dec			ebx

		paddd		mm6,mm0
		jne			xloop

		movd		eax,mm6
		psrlq		mm6,32
		movd		ecx,mm6
		add			eax,ecx

		pop			ebx
		ret
	}
}
#endif

static long computeScanImprovement(Pixel8 *src1, Pixel8 *src2, PixOffset pitch, PixDim w) {
	long imp = 0;

	// now using original intended algorithm, plus checking if the second frame is also combed
	// so it doesn't easily take the frame to be dropped as the frame to be decombed. 
	// Check below to see fix...  This actually works very well! Beats any commercial
	// software out there!
	//
	// Without the fix, the reason the broken algorithm sorta worked is because, in the 
	// sequence [A1/A2] [A1/B2], if A2 sorta looked like B2, it would actually take the 
	// right offset... but when scene changes a lot, this does not work at all. 
	//
	// Samuel Audet <guardia@cam.órg>

	w = -w;
	do {
		int rA = src1[0];
		int rB = src1[pitch];
		int rC = src1[pitch*2];
		int rD = src2[0];
		int rE = src2[pitch];
		int rF = src2[pitch*2];

		imp += sq(rA + rC - 2*rB)		// combing in current frame
            + sq(rD + rF - 2*rE)		// combing in second frame
			- sq(rA + rC - 2*rE);		// combing in merged frame

		src1++;
		src2++;
	} while(++w);

	return imp;
}

void CVideoTelecineRemover::ProcessIn(VBitmap *pIn, long lFrameNum) {
	Pixel8 *src1, *src2;
	PixDim h;
	__int64 field1=0, field2=0;

	vb.data = (Pixel *)(pMemBlock + vb.pitch*vb.h * nCurrentIn);

	vb.BitBlt(0, 0, pIn, 0, 0, -1, -1);

	lFrameNums[nCurrentIn] = lFrameNum;

	if (fOffsetLocked) {
		if (++nCurrentIn == 10)
			nCurrentIn = 0;
		return;
	}

	// We skip the top and bottom 8 lines -- they're usually full of head noise.

	{
		long longs = (vb.w*3)/4;

		h = (vb.h-2)/2 - 8;
		src1 = (Pixel8 *)(pMemBlock + vb.pitch*(vb.h * nCurrentIn + 8));
		src2 = (Pixel8 *)(pMemBlock + vb.pitch*(vb.h * ((nCurrentIn+9)%10) + 8));

		if (MMX_enabled) {
			do {
				field1 += computeScanImprovementMMX(src1, src2, vb.pitch, longs);
				field2 += computeScanImprovementMMX(src1+vb.pitch, src2+vb.pitch, vb.pitch, longs);

				src1 += vb.pitch*2;
				src2 += vb.pitch*2;
			} while(--h);

			__asm emms
		} else {
			do {
				field1 += computeScanImprovement(src1, src2, vb.pitch, longs*4);
				field2 += computeScanImprovement(src1+vb.pitch, src2+vb.pitch, vb.pitch, longs*4);

				src1 += vb.pitch*2;
				src2 += vb.pitch*2;
			} while(--h);
		}
	}

	if (field1 < 0)
		field1 = 0;

	if (field2 < 0)
		field2 = 0;

	_RPT2(0,"%16d %16d\n", (long)sqrt(field1), (long)sqrt(field2));

	nCombVals[(nCurrentIn+9)%10][0] = (long)sqrt(field1);
	nCombVals[(nCurrentIn+9)%10][1] = (long)sqrt(field2);

	if (++nCurrentIn == 10)
		nCurrentIn = 0;

	if (nCurrentIn == 0 || nCurrentIn == 5) {
		int i;
		long best_score = 0;
		int best_offset = -1;
		bool best_polarity = false;

		for(i=0; i<5; i++) {
			long v1 = nCombVals[(nCurrentIn+4+i)%10][0];
			long v2 = nCombVals[(nCurrentIn+4+i)%10][1];

			if (v1 > best_score) {
				best_offset = (i+4)%5;
				best_polarity = true;
				best_score = v1;
			}

			if (v2 > best_score) {
				best_offset = (i+4)%5;
				best_polarity = false;
				best_score = v2;
			}
		}

		_RPT4(0,"----------- %d %d [%d %d]\n", best_offset, best_polarity, nNewCombOffset1, fNewPolarity);

		fDropMode = fNewDropMode;
		nCombOffset1 = nNewCombOffset1;
		nCombOffset2 = nNewCombOffset2;
		fInvertPolarity = fNewPolarity;

		if (best_offset == -1) {
			fNewDropMode = true;
			nNewCombOffset1 = 0;
			nNewCombOffset2 = 1;
		} else {
			fNewDropMode = false;
			nNewCombOffset1 = best_offset;
			nNewCombOffset2 = (best_offset+1) % 5;
			fNewPolarity = best_polarity;
		}
	}
}

static void __declspec(naked) DeBlendMMX_32_24(void *dst, void *src1, void *src2, void *src3, void *src4, long w3, long h, long spitch, long dmodulo) {
	static const __int64 one = 0x0001000100010001i64;
	__asm {
		push		ebp
		push		edi
		push		esi
		push		ebx

		mov			ebp,[esp+24+16]
		mov			edi,[esp+4+16]
		mov			eax,[esp+8+16]
		mov			ebx,[esp+12+16]
		mov			ecx,[esp+16+16]
		mov			edx,[esp+20+16]
		sub			eax,ebp
		sub			ebx,ebp
		sub			ecx,ebp
		sub			edx,ebp
		pxor		mm7,mm7
yloop:
		mov			ebp,[esp+24+16]
xloop:
		movd		mm0,[eax+ebp]
		movd		mm1,[ebx+ebp]
		movd		mm2,[ecx+ebp]
		movd		mm3,[edx+ebp]
		punpcklbw	mm0,mm7			;mm0 = A = a
		punpcklbw	mm1,mm7			;mm1 = B = (a+b)/2
		punpcklbw	mm2,mm7			;mm2 = C = (b+c)/2
		punpcklbw	mm3,mm7			;mm3 = D = c
		paddw		mm0,mm3			;mm0 = A+D = a+c
		paddw		mm1,mm2			;mm1 = B+C = (a+b)/2 + (b+c)/2 = b + (a+c)/2
		paddw		mm0,one
		psrlw		mm0,1			;mm0 = (a+c)/2
		psubw		mm1,mm0			;mm1 = [B1][R0][G0][B0] *done

		movd		mm0,[eax+ebp+4]
		movd		mm4,[ebx+ebp+4]
		movd		mm2,[ecx+ebp+4]
		movd		mm3,[edx+ebp+4]
		punpcklbw	mm0,mm7			;mm0 = A = a
		punpcklbw	mm4,mm7			;mm4 = B = (a+b)/2
		punpcklbw	mm2,mm7			;mm2 = C = (b+c)/2
		punpcklbw	mm3,mm7			;mm3 = D = c
		paddw		mm0,mm3			;mm0 = A+D = a+c
		paddw		mm4,mm2			;mm4 = B+C = (a+b)/2 + (b+c)/2 = b + (a+c)/2
		paddw		mm0,one
		psrlw		mm0,1			;mm0 = (a+c)/2
		psubw		mm4,mm0			;mm4 = [G2][B2][R1][G1]

		movd		mm0,[eax+ebp+8]
		movd		mm5,[ebx+ebp+8]
		movd		mm2,[ecx+ebp+8]
		movd		mm3,[edx+ebp+8]
		punpcklbw	mm0,mm7			;mm0 = A = a
		punpcklbw	mm5,mm7			;mm1 = B = (a+b)/2
		punpcklbw	mm2,mm7			;mm2 = C = (b+c)/2
		punpcklbw	mm3,mm7			;mm3 = D = c
		paddw		mm0,mm3			;mm0 = A+D = a+c
		paddw		mm5,mm2			;mm1 = B+C = (a+b)/2 + (b+c)/2 = b + (a+c)/2
		paddw		mm0,one
		psrlw		mm0,1			;mm0 = (a+c)/2
		psubw		mm5,mm0			;mm5 = [R3][G3][B3][R2]

		movq		mm6,mm4			;mm6 = [G2][B2][R1][G1]
		psrlq		mm4,32			;mm4 = [  ][  ][G2][B2]

		psllq		mm6,16			;mm6 = [B2][R1][G1][  ]
		movq		mm2,mm1			;mm2 = [B1][R0][G0][B0]

		movq		mm0,mm5			;mm0 = [R3][G3][B3][R2]
		psrlq		mm2,48			;mm2 = [  ][  ][  ][B1]

		psllq		mm5,32			;mm5 = [B3][R2][  ][  ]
		por			mm2,mm6			;mm2 = [B2][R1][G1][B1] *done

		por			mm4,mm5			;mm4 = [B3][R2][G2][B2] *done
		packuswb	mm1,mm2			;mm0 = [B2][R1][G1][B1][B1][R0][G0][B0]

		psrlq		mm0,16			;mm5 = [  ][R3][G3][B3] *done

		movq		[edi+0],mm1
		packuswb	mm4,mm0			;mm4 = [  ][R3][G3][B3][B3][R2][G2][B2]

		movq		[edi+8],mm4

		add			edi,16

		add			ebp,12
		jne			xloop

		mov			ebp,[esp+32+16]
		add			edi,[esp+36+16]
		add			eax,ebp
		add			ebx,ebp
		add			ecx,ebp
		add			edx,ebp

		dec			dword ptr [esp+28+16]
		jne			yloop
		emms

		pop			ebx
		pop			esi
		pop			edi
		pop			ebp
		ret
	}
}

long CVideoTelecineRemover::ProcessOut(VBitmap *pOut) {

	if (++nCurrentOut >= 10)
		nCurrentOut = 0;

	if (nLag) {
		--nLag;
		return -1;
	}

	// Input frames:	[A1/A2] [A1/B2] [B1/C2] [C1/C2] [D1/D2]
	// Action:			copy    decomb  drop    copy    copy

	if ((nCurrentOut == nCombOffset1 || nCurrentOut == nCombOffset1+5) && !fDropMode) {
		// First combed frame; reconstruct.

		if (fDecomb) {
			VBitmap vb_in, vb_out;

			// Copy bottom field.

			vb_in			= vb;
			vb_in.data		= (Pixel *)(pMemBlock + vb.pitch * (vb.h*((nCurrentOut+(fInvertPolarity?1:0))%10) + (vb.h & 1)));
			vb_in.modulo	+= vb.pitch;
			vb_in.pitch		*= 2;
			vb_in.h			= (vb_in.h + 1)/2;

			vb_out			= *pOut;
			vb_out.modulo	+= vb_out.pitch;
			vb_out.pitch	*= 2;
			vb_out.h		= (vb_out.h + 1)/2;

			vb_out.BitBlt(0, 0, &vb_in, 0, 0, -1, -1);
	//		vb_out.RectFill(0, 0, -1, -1, 0x00ff00);

			// Copy top field.

			vb_in.data		= (Pixel *)(pMemBlock + vb.pitch * (vb.h*((nCurrentOut+(fInvertPolarity?0:1))%10) + 1 - (vb.h & 1)));
			vb_in.h			= vb.h/2;

			vb_out.data		= (Pixel *)((char *)pOut->data + pOut->pitch*(1-(pOut->h & 1)));
			vb_out.h		= pOut->h/2;

			vb_out.BitBlt(0, 0, &vb_in, 0, 0, -1, -1);
	//		vb_out.RectFill(0, 0, -1, -1, 0xff0000);
		} else {
			_RPT2(0,"Recon: %d %d\n", nCurrentIn, nCurrentOut);

			DeBlendMMX_32_24(
					pOut->data,
					pMemBlock + vb.pitch * (vb.h * ((nCurrentOut+9)%10)),
					pMemBlock + vb.pitch * (vb.h *   nCurrentOut       ),
					pMemBlock + vb.pitch * (vb.h * ((nCurrentOut+1)%10)),
					pMemBlock + vb.pitch * (vb.h * ((nCurrentOut+2)%10)),
					-vb.w*3,
					vb.h,
					vb.pitch,
					pOut->modulo);
		}

		return lFrameNums[nCurrentOut];

	} else if (nCurrentOut == nCombOffset2 || nCurrentOut == nCombOffset2+5) {
		// Second combed frame; drop.
		return -1;
	} else {
		// Uncombed, unduplicated frame.

		vb.data = (Pixel *)(pMemBlock + vb.pitch*vb.h * nCurrentOut);

		pOut->BitBlt(0, 0, &vb, 0, 0, -1, -1);

		return lFrameNums[nCurrentOut];
	}
}
