/****************************************************************************
 *
 *  GETFRAME.CPP
 *
 *  this file contains the GetFrame APIs
 *
 *      AVIStreamGetFrameOpen
 *      AVIStreamGetFrameClose
 *      AVIStreamGetFrame
 *
 *  it also contains the default GetFrame implemenation
 *
 *      GetFrameDef
 *
 ***************************************************************************/

#include <win32.h>
#include <vfw.h>
#include <memory.h>             // for _fmemset

#include "debug.h"              // for good ol' DPF()


/****************************************************************************
 *
 ***************************************************************************/

//!!! ACK
#define AVISF_VIDEO_PALCHANGES          0x00010000

#define ERR_FAIL   ResultFromScode(E_FAIL)
#define ERR_MEMORY ResultFromScode(E_OUTOFMEMORY)

#define WIDTHBYTES(i)       ((UINT)((i+31)&(~31))/8)
#define DIBWIDTHBYTES(lpbi) (UINT)WIDTHBYTES((UINT)(lpbi)->biWidth * (UINT)(lpbi)->biBitCount)

/****************************************************************************
 *
 *  class for default IGetFrame
 *
 ***************************************************************************/

class FAR GetFrameDef : public IGetFrame
{
public:
    GetFrameDef(IAVIStream FAR *pavi=NULL);

public:
    // IUnknown stuff

    STDMETHODIMP QueryInterface(REFIID riid, LPVOID FAR* ppv);
    STDMETHODIMP_(ULONG) AddRef();
    STDMETHODIMP_(ULONG) Release();

    // IGetFrame stuff.

    STDMETHODIMP Begin              (LONG lStart, LONG lEnd, LONG lRate);
    STDMETHODIMP End                ();

    STDMETHODIMP SetFormat          (LPBITMAPINFOHEADER lpbi, LPVOID lpBits, int x, int y, int dx, int dy);

    STDMETHODIMP_(LPVOID) GetFrame  (LONG lPos);

private:
    ~GetFrameDef();
    void FreeStuff();

    // for AddRef
    ULONG   ulRefCount;

    // instance data.
    BOOL                        fBegin;         // inside of Begin/End
    BOOL                        fFmtChanges;    // file has format changes.

    PAVISTREAM			pavi;
    LONG                        lFrame;         // last frame decompressed

    LPVOID                      lpBuffer;       // read buffer.
    LONG                        cbBuffer;       // size of read buffer
    LPVOID                      lpFormat;       // stream format
    LONG                        cbFormat;       // size of format

    LPVOID                      lpFrame;        // the frame (format)
    LPVOID                      lpBits;         // the frame (bits)
    HIC                         hic;            // decompress handle

    BOOL                        fDecompressEx;  // using ICDecompressEx
    int                         x,y,dx,dy;      // where to decompress

    // to watch for the format changing.
    DWORD			dwFormatChangeCount;
    DWORD			dwEditCount;
};

/****************************************************************************

 IUnknown stuff.

 ***************************************************************************/

STDMETHODIMP GetFrameDef::QueryInterface(REFIID riid, LPVOID FAR* ppv)
{
    if (riid == IID_IGetFrame ||
        riid == IID_IUnknown) {     //!!! should we do Unknown or pass on?

        *ppv = (LPVOID)this;
        AddRef();
        return ResultFromScode(S_OK);
    }
    else if (pavi) {
        return pavi->QueryInterface(riid, ppv);
    }
    else {
        *ppv = NULL;
        return ResultFromScode(E_NOINTERFACE);
    }
}

STDMETHODIMP_(ULONG) GetFrameDef::AddRef()
{
    return ulRefCount++;
}

STDMETHODIMP_(ULONG) GetFrameDef::Release()
{
    if (--ulRefCount == 0) {
        delete this;
        return 0;
    }
    return ulRefCount;
}

/****************************************************************************
 ***************************************************************************/

GetFrameDef::GetFrameDef(IAVIStream FAR *pavi)
{
    this->pavi = pavi;

    ulRefCount = 1;

    fBegin = FALSE;
    fFmtChanges = FALSE;
    fDecompressEx = FALSE;

    lFrame = -4242;

    lpBuffer = NULL;
    lpFormat = NULL;
    cbBuffer = 0;
    cbFormat = 0;

    lpFrame = NULL;
    lpBits  = NULL;
    hic     = NULL;

    if (this->pavi == NULL)
        return;

    pavi->AddRef();
}

/****************************************************************************
 ***************************************************************************/

GetFrameDef::~GetFrameDef()
{
    FreeStuff();

    if (pavi)
        pavi->Release();
}

/****************************************************************************
 ***************************************************************************/

void GetFrameDef::FreeStuff()
{
    if (this->lpFrame && this->lpFrame != this->lpFormat) {
        GlobalFreePtr(this->lpFrame);
        this->lpFrame = 0;
    }

    if (this->lpFormat) {
        GlobalFreePtr(this->lpFormat);
        this->lpFormat = 0;
    }

    if (this->hic) {

        if (this->fDecompressEx)
            ICDecompressExEnd(this->hic);
        else
            ICDecompressEnd(this->hic);

        ICClose(this->hic);
        this->hic = 0;
    }
}

/****************************************************************************
 ***************************************************************************/

STDMETHODIMP GetFrameDef::SetFormat(LPBITMAPINFOHEADER lpbi, LPVOID lpBits, int x, int y, int dx, int dy)
{
    LPBITMAPINFOHEADER	lpbiC;
    LPBITMAPINFOHEADER	lpbiU;
    LRESULT		dw;
    DWORD		fccHandler;
    AVISTREAMINFOW      info;
    BOOL                fScreen;

    //
    // lpbi == AVIGETFRAMEF_BESTDISPLAYFMT means choose the best format for the
    // screen.
    //
    if (fScreen = (lpbi == (LPBITMAPINFOHEADER)AVIGETFRAMEF_BESTDISPLAYFMT))
        lpbi = NULL;

    //
    // get the vital stats
    //
    _fmemset(&info, 0, sizeof(info));
    pavi->Info(&info, sizeof(info));

    //
    //  is this a video stream?
    //
    if (info.fccType != streamtypeVIDEO)
        return ERR_FAIL;

    this->fBegin = FALSE;
    this->fFmtChanges = (info.dwFlags & AVISF_VIDEO_PALCHANGES) != 0;

    this->dwEditCount = info.dwEditCount;
    this->dwFormatChangeCount = info.dwFormatChangeCount;

    //
    // get the stream format
    //
    if (this->lpFormat == NULL) {

        //
        // alocate a read buffer.
        //
        this->cbBuffer = (LONG)info.dwSuggestedBufferSize;

	if (this->cbBuffer == 0)
	    this->cbBuffer = 1024;

        AVIStreamFormatSize(this->pavi,
			    AVIStreamStart(this->pavi),
                            &this->cbFormat);

        this->lpFormat = GlobalAllocPtr(GHND,this->cbFormat + this->cbBuffer);

	if (this->lpFormat == NULL)
	    goto error;

	AVIStreamReadFormat(this->pavi, AVIStreamStart(this->pavi),
			    this->lpFormat, &this->cbFormat);

	this->lpBuffer = (LPBYTE)this->lpFormat+this->cbFormat;
    }

    lpbiC = (LPBITMAPINFOHEADER)this->lpFormat;

    //
    // do standard BITMAPINFO header cleanup!
    //
    if (lpbiC->biClrUsed == 0 && lpbiC->biBitCount <= 8)
	lpbiC->biClrUsed = (1 << (int)lpbiC->biBitCount);

    if (lpbiC->biSizeImage == 0 && lpbiC->biCompression == BI_RGB)
	lpbiC->biSizeImage = DIBWIDTHBYTES(lpbiC) * lpbiC->biHeight;

    //
    // if the stream is uncompressed, we dont need a decompress buffer
    // make sure the caller hs not suggested a format first.
    //
    if (lpbiC->biCompression == 0 && lpBits == NULL) {

	if (lpbi == NULL ||
	   (lpbi->biCompression == lpbiC->biCompression &&
	    lpbi->biWidth	== lpbiC->biWidth &&
	    lpbi->biHeight	== lpbiC->biHeight &&
	    lpbi->biBitCount	== lpbiC->biBitCount)) {


	    this->lpBits = (LPBYTE)lpbiC + (int)lpbiC->biSize +
		(int)lpbiC->biClrUsed * sizeof(RGBQUAD);

	    goto done;
	}
    }

    //
    // alocate the decompress buffer.
    //
    if (this->lpFrame == NULL) {

        this->lpFrame = GlobalAllocPtr(GHND,
            sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD));

        if (this->lpFrame == NULL) {
	    DPF("GetFrameInit: Can't allocate frame buffer!\n");
	    goto error;
        }
    }

    lpbiC = (LPBITMAPINFOHEADER)this->lpFormat;
    lpbiU = (LPBITMAPINFOHEADER)this->lpFrame;

    if (this->hic == NULL) {

        if (lpbiC->biCompression == 0)
            fccHandler = mmioFOURCC('D','I','B',' ');
        else if (lpbiC->biCompression == BI_RLE8)
            fccHandler = mmioFOURCC('R','L','E',' ');
        else
            fccHandler = info.fccHandler;

        if (lpbi) {
            if (lpbi->biWidth == 0)
                lpbi->biWidth = lpbiC->biWidth;

            if (lpbi->biHeight == 0)
                lpbi->biHeight = lpbiC->biHeight;
        }

        this->hic = ICDecompressOpen(ICTYPE_VIDEO, /*info.fccType,*/
                                   fccHandler,lpbiC,lpbi);

        if (this->hic == NULL) {
	    DPF("GetFrameInit: Can't find decompressor!\n");
	    goto error;
        }
    }

    if (lpbi) {
        if (lpbi->biClrUsed == 0 && lpbi->biBitCount <= 8)
            lpbi->biClrUsed = (1 << (int)lpbi->biBitCount);
	
        hmemcpy(lpbiU,lpbi,lpbi->biSize + lpbi->biClrUsed * sizeof(RGBQUAD));

        if (lpbi->biBitCount <= 8) {
            ICDecompressGetPalette(this->hic,lpbiC,lpbiU);
        }
    } else if (fScreen) {

        ICGetDisplayFormat(this->hic, lpbiC, lpbiU, 0, dx, dy);

    } else {
        dw = ICDecompressGetFormat(this->hic,lpbiC,lpbiU);

	if ((LONG)dw < ICERR_OK)
	    goto error;
    }

    //
    // do standard BITMAPINFO header cleanup!
    //
    if (lpbiU->biClrUsed == 0 && lpbiU->biBitCount <= 8)
        lpbiU->biClrUsed = (1 << (int)lpbiU->biBitCount);

    if (lpbiU->biSizeImage == 0 && lpbiU->biCompression == BI_RGB)
        lpbiU->biSizeImage = DIBWIDTHBYTES(lpbiU) * lpbiU->biHeight;

    //
    // if we were passed a bits pointer, use it else re-alloc lpFrame
    // to contain the bits too.
    //
    if (lpBits) {
        this->lpBits = lpBits;
    }
    else {
        this->lpFrame = GlobalReAllocPtr(this->lpFrame,lpbiU->biSize +
            lpbiU->biSizeImage +
            lpbiU->biClrUsed * sizeof(RGBQUAD), GMEM_MOVEABLE);

        if (this->lpFrame == NULL) {
            DPF("GetFrameInit: Can't resize frame buffer!\n");
	    goto error;
        }

        lpbiU = (LPBITMAPINFOHEADER)this->lpFrame;

        this->lpBits = (LPBYTE)lpbiU + (int)lpbiU->biSize +
                (int)lpbiU->biClrUsed * sizeof(RGBQUAD);
    }

    //
    // use ICDecompressEx if we need to.  we need DecompressEx if
    // we are decompressing into a smaller area of the DIB, not the
    // whole surface.
    //
    if (dx == -1)
        dx = (int)lpbiU->biWidth;

    if (dy == -1)
        dy = (int)lpbiU->biHeight;

    this->fDecompressEx = (x != 0 || y != 0 ||
        dy != (int)lpbiU->biHeight || dx != (int)lpbiU->biWidth);

    if (this->fDecompressEx) {

        this->x = x;
        this->y = y;
        this->dx = dx;
        this->dy = dy;

        dw = ICDecompressExBegin(this->hic, 0,
            lpbiC, NULL, 0, 0, lpbiC->biWidth, lpbiC->biHeight,
            lpbiU, NULL, x, y, dx, dy);
    }
    else {
        dw = ICDecompressBegin(this->hic,lpbiC,lpbiU);
    }

    if (dw != ICERR_OK) {
        DPF("GetFrameSetFormat: ICDecompressBegin failed!\n");
	goto error;
    }

done:
    this->lFrame = -4224;   // bogus value
    return AVIERR_OK;

error:
    FreeStuff();
    return ERR_FAIL;
}

/****************************************************************************
 ***************************************************************************/

STDMETHODIMP GetFrameDef::Begin(LONG lStart, LONG lEnd, LONG lRate)
{
    fBegin = TRUE;
    GetFrame(lStart);

    return AVIERR_OK;
}

/****************************************************************************
 ***************************************************************************/

STDMETHODIMP GetFrameDef::End()
{
    fBegin = FALSE;
    return AVIERR_OK;
}

/****************************************************************************
 ***************************************************************************/

STDMETHODIMP_(LPVOID) GetFrameDef::GetFrame(LONG lPos)
{
    LPBITMAPINFOHEADER	    lpbiC;
    LPBITMAPINFOHEADER	    lpbiU;
    LONG                    l;
    LONG                    lKey;
    LONG		    lBytes;
    LONG		    lSize;
    LONG		    lRead;
    LRESULT                 err;
    AVISTREAMINFOW          info;
    HRESULT		    hr;

    if (!this->pavi) {
	DPF("AVIStreamGetFrame: bad pointer\n");
	return NULL;
    }

    if (this->lpFormat == NULL) {
        return NULL;
    }

    //
    // if we are not in a Begin/End pair check for the format changing etc.
    //
    if (!this->fBegin) {

        _fmemset(&info, 0, sizeof(info));
        this->pavi->Info(&info, sizeof(info));

        if (info.dwFormatChangeCount != dwFormatChangeCount) {

            DPF("AVIStreamGetFrame: format has changed\n");

	    if (this->lpFrame) {
		BITMAPINFOHEADER bi = *((LPBITMAPINFOHEADER)this->lpFrame);

		FreeStuff();    // nuke it all.

		if (SetFormat(&bi, NULL, 0, 0, -1, -1) != 0 &&
		    SetFormat(NULL, NULL, 0, 0, -1, -1) != 0)

		    return NULL;
	    } else {
		if (SetFormat(NULL, NULL, 0, 0, -1, -1) != 0) {
		    return NULL;
		}
	    }
        }

        if (info.dwEditCount != dwEditCount) {
            DPF("AVIStreamGetFrame: stream has been edited (%lu)\n", info.dwEditCount);
            dwEditCount = info.dwEditCount;
            this->lFrame = -4224;     // Invalidate the cached frame
        }
    }

    //
    // quick check for the last frame.
    //
    if (this->lFrame == lPos)
        return this->hic ? this->lpFrame : this->lpFormat;

    //
    // locate the nearest key frame.
    //
    lKey = AVIStreamFindSample(this->pavi, lPos, FIND_KEY|FIND_PREV);

    //
    // either lPos was out of range or some internal error!
    //
    if (lKey == -1) {
	DPF("AVIStreamGetFrame: Couldn't find key frame!\n");
	return NULL;
    }

    //
    // we need to go back to the specifed key frame
    // or our current frame witch ever is closer
    //
    if (this->lFrame < lPos && this->lFrame >= lKey)
        lKey = this->lFrame + 1;

    lpbiC = (LPBITMAPINFOHEADER)this->lpFormat;
    lpbiU = (LPBITMAPINFOHEADER)this->lpFrame;

    //
    // decompress frame data from key frame to current frame.
    //
    for (l=lKey; l<=lPos; l++) {

        //
	// go read the format and call ICDecompressGetPalette() so
	// if the palette changes things will work.
	//
        if (this->fFmtChanges) {

            AVIStreamReadFormat(this->pavi, l, lpbiC, &this->cbFormat);
	
	    if (lpbiU && lpbiU->biBitCount <= 8) {
                ICDecompressGetPalette(this->hic,lpbiC,lpbiU);
	    }
	}

try_read_again:
        hr = AVIStreamRead(this->pavi, l, 1,
            this->lpBuffer, this->cbBuffer, &lBytes, &lRead);

        //
        // the read failed, mabey our buffer was too small
        // or it was a real error.
        //
        if (hr != NOERROR) {

            DPF("AVIStreamGetFrame: AVIStreamRead returns %lx\n", (DWORD) hr);

            lSize = 0;
            hr = AVIStreamSampleSize(this->pavi, l, &lSize);

            if (lSize > this->cbBuffer) {
                LPVOID lp;

                DPF("AVIStreamGetFrame: re-sizing read buffer from %ld to %ld\n", this->cbBuffer, lSize);

		lp = GlobalReAllocPtr(this->lpFormat,this->cbFormat+lSize,0);

                if (lp == NULL) {
                    DPF("AVIStreamGetFrame: Couldn't resize buffer\n");
                    return NULL;
                }

		this->lpFormat = lp;
		lpbiC = (LPBITMAPINFOHEADER)this->lpFormat;
		this->lpBuffer = (LPBYTE)lp + this->cbFormat;
                this->cbBuffer = lSize;

                goto try_read_again;
            }
	}

	if (lRead != 1) {
	    DPF("AVIStreamGetFrame: AVIStreamRead failed!\n");
	    return NULL;
	}

	if (lBytes == 0)
	    continue;

	lpbiC->biSizeImage = lBytes;

	if (this->hic == NULL) {
	    this->lFrame = lPos;
	    return this->lpFormat;
	}
	else if (this->fDecompressEx) {
            err = ICDecompressEx(this->hic,0,
                lpbiC,this->lpBuffer,
                0,0,(int)lpbiC->biWidth,(int)lpbiC->biHeight,
                lpbiU,this->lpBits,
                this->x,this->y,this->dx,this->dy);
        }
        else {
            err = ICDecompress(this->hic,0,
                lpbiC,this->lpBuffer,lpbiU,this->lpBits);
        }

        // !!! Error check?

        if (err < 0) {
	}
    }

    this->lFrame = lPos;
    return this->hic ? this->lpFrame : this->lpFormat;
}

/********************************************************************
* @doc EXTERNAL AVIStreamGetFrameOpen
*
* @api PGETFRAME | AVIStreamGetFrameOpen | This functions prepares
*      to decompress video frames from the stream specified.
*
* @parm PAVISTREAM | pavi | Specifies a pointer to the
*       stream used as the video source.
*
* @parm LPBITMAPINFOHEADER | lpbiWanted | Specifies a pointer to
*       a structure defining the desired  video format.  If this is NULL,
*       a default format is used.
*
* @rdesc Returns a GetFrame object, which can be used with
*	<f AVIStreamGetFrame>.
*
*	If the system can't find decompressor that can decompress the stream
*	to the format given, or to any RGB format, the function returns NULL.
*
* @comm The <p pavi> parameter must specify a video stream.
*
*	This is essentially just a helper function to handle a simple form
*	of decompression.
*
* @xref <f AVIStreamGetFrame> <f AVIStreamGetFrameClose>
**********************************************************************/
STDAPI_(PGETFRAME) AVIStreamGetFrameOpen(PAVISTREAM pavi, LPBITMAPINFOHEADER lpbiWanted)
{
    PGETFRAME pgf=NULL;

    //
    // first ask the IAVIStream object if it can handle IGetFrame and
    // if it can let it do it.
    //
    pavi->QueryInterface(IID_IGetFrame, (LPVOID FAR *)&pgf);

    if (pgf == NULL) {
        //
        // the stream can't do it, make our own object.
        //
        pgf = new GetFrameDef(pavi);
    }

    //
    // set the format the caller wants
    //
    if (pgf->SetFormat(lpbiWanted, NULL, 0, 0, -1, -1)) {
        DPF("AVIStreamGetFrameOpen: unable to set format\n");
        pgf->Release();
        return NULL;
    }

    return pgf;
}

/********************************************************************
* @doc EXTERNAL AVIStreamGetFrameClose
*
* @api LONG | AVIStreamGetFrameClose | This function releases resources
*	used to decompress video frames.
*
* @parm PGETFRAME | pget | Specifies a handle returned from <f AVIStreamGetFrameOpen>.
*	After calling this function, the handle is invalid.
*
* @rdesc Returns an error code.
*
* @xref <f AVIStreamGetFrameOpen> <f AVIStreamGetFrame>
**********************************************************************/
STDAPI AVIStreamGetFrameClose(PGETFRAME pgf)
{
    if (pgf)
        pgf->Release();

    return AVIERR_OK;
}

/********************************************************************
* @doc EXTERNAL AVIStreamGetFrame
*
* @api LPVOID | AVIStreamGetFrame | This function returns a pointer to
*	a decompressed frame of video.
*
* @parm PGETFRAME | pgf | Specifies a pointer to a GetFrame object.
*
* @parm LONG | lPos | Specifies the position of desired frame in samples.
*
* @rdesc Returns NULL on error; otherwise it returns a far pointer
*        to the frame data.  The returned data is a packed DIB.
*
* @comm The returned frame is valid only until the next call
*	to <f AVIStreamGetFrame> or <f AVIStreamGetFrameClose>.
*
* @xref <f AVIStreamGetFrameOpen>
**********************************************************************/
STDAPI_(LPVOID) AVIStreamGetFrame(PGETFRAME pgf, LONG lPos)
{
    if (pgf == NULL)
        return NULL;

    return pgf->GetFrame(lPos);
}

// !!! Do we need an AVIStreamGetFrameSetFormat?
