////////////////////////////////////////////////////////
//
// GEM - Graphics Environment for Multimedia
//
// mark@danks.org
//
// Implementation file 
//
//    Copyright (c) 1997-1999 Mark Danks.
//    For information on usage and redistribution, and for a DISCLAIMER OF ALL
//    WARRANTIES, see the file, "GEM.LICENSE.TERMS" in this distribution.
//
/////////////////////////////////////////////////////////

#include "pix_movie.h"

#include "Base/GemCache.h"
#include "Base/GemMan.h"
#include "Base/GemWinCreate.h"

static inline int powerOfTwo(int value)
{
    int x = 1;
    while(x < value) x <<= 1;
    return(x);
}

CPPEXTERN_NEW_WITH_ONE_ARG(pix_movie, t_symbol *, A_DEFSYM)

/////////////////////////////////////////////////////////
//
// pix_movie
//
/////////////////////////////////////////////////////////
// Constructor
//
/////////////////////////////////////////////////////////
pix_movie :: pix_movie(t_symbol *filename)
    	   : m_haveMovie(0), m_swap(1), m_colorSwap(0),
		     m_xRatio(1.f), m_yRatio(1.f),
		     m_getFrame(NULL), m_reqFrame(0), m_curFrame(0), m_textureObj(0)
{
	m_dataSize[0] = m_dataSize[1] = m_dataSize[2] = 0;

    inlet_new(this->x_obj, &this->x_obj->ob_pd, gensym("float"), gensym("img_num"));

	// create an outlet to send out how many frames are in the movie
	m_outNumFrames = outlet_new(this->x_obj, 0);

	// initialize the pix block data
    m_pixBlock.image.data = NULL;
    m_pixBlock.image.xsize = 0;
    m_pixBlock.image.ysize = 0;
    m_pixBlock.image.csize = 3;
    m_pixBlock.image.format = GL_BGR_EXT;
    m_pixBlock.image.type = GL_UNSIGNED_BYTE;

	// increment the AVI library reference count
	AVIFileInit();

    // make sure that there are some characters
    if (filename->s_name[0])
		openMess(filename);
}

/////////////////////////////////////////////////////////
// Destructor
//
/////////////////////////////////////////////////////////
pix_movie :: ~pix_movie()
{
	outlet_free(m_outNumFrames);

    // Clean up the movie
    if (m_haveMovie)
    {
	  	AVIStreamGetFrameClose(m_getFrame);
		m_haveMovie = false;
		m_getFrame = NULL;
    }
	AVIFileExit();
}

/////////////////////////////////////////////////////////
// openMess
//
/////////////////////////////////////////////////////////
void pix_movie :: openMess(t_symbol *filename)
{
    char buf[MAXPDSTRING];
    canvas_makefilename(getCanvas(), filename->s_name, buf, MAXPDSTRING);

	// Opens the AVI stream
	if (AVIStreamOpenFromFile(&m_streamVid, buf, streamtypeVIDEO, 0, OF_READ, NULL))
	{
		error("GEM: pix_movie: Unable to open file: %s", buf);
		return;
	}

	if (m_haveMovie)
	  	AVIStreamGetFrameClose(m_getFrame);

	// Create the PGETFRAME
	m_getFrame = AVIStreamGetFrameOpen(m_streamVid,NULL);
	m_haveMovie = 1;
	m_reqFrame = 0;
	m_curFrame = -1;

	// get all of the information about the stream
	AVISTREAMINFO psi;
	AVIStreamInfo(m_streamVid, &psi, sizeof(AVISTREAMINFO));

	// Get the length of the movie
	m_numFrames = psi.dwLength;
	
	m_xsize = psi.rcFrame.right - psi.rcFrame.left;
	m_ysize = psi.rcFrame.bottom - psi.rcFrame.top;

	// allocate a buffer that is the next power of two of the movie
	const int powXSize = powerOfTwo(m_xsize);
	const int powYSize = powerOfTwo(m_ysize);

	if (m_pixBlock.image.xsize != powXSize ||
		m_pixBlock.image.ysize != powYSize)
	{
		delete [] m_pixBlock.image.data;
		m_pixBlock.image.xsize = powXSize;
		m_pixBlock.image.ysize = powYSize;
		int dataSize = m_pixBlock.image.xsize * m_pixBlock.image.ysize * m_pixBlock.image.csize;
		m_pixBlock.image.data = new unsigned char[dataSize];
		memset(m_pixBlock.image.data, 0, dataSize);
	}

	// ratio for the texture map coordinates
	m_xRatio = (float)m_xsize / (float)powXSize;
	m_yRatio = (float)m_ysize / (float)powYSize;
	
	m_coords[0].s = 0.f;
	m_coords[0].t = 0.f;
	
	m_coords[1].s = m_xRatio;
	m_coords[1].t = 0.f;
	
	m_coords[2].s = m_xRatio;
	m_coords[2].t = m_yRatio;
	
	m_coords[3].s = 0.f;
	m_coords[3].t = m_yRatio;

	outlet_float(m_outNumFrames, (float)m_numFrames);
	post("GEM: pix_movie: Loaded file: %s with %d frames", buf, m_numFrames);
}

/////////////////////////////////////////////////////////
// render
//
/////////////////////////////////////////////////////////
void pix_movie :: render(GemState *state)
{
	if (!m_haveMovie)
		return;

	// do we actually need to get a new frame from the movie
	int newImage = 0;
	unsigned char *pt;
	if (m_reqFrame != m_curFrame)
	{
		// get the frame
		newImage = 1;
		m_curFrame = m_reqFrame;
		pt = (unsigned char *)AVIStreamGetFrame(m_getFrame, m_curFrame);

		// copy over all of the data
	}

    m_pixBlock.newimage = newImage;
	state->image = &m_pixBlock;
	state->texture = 1;
	state->texCoords = m_coords;
	state->numTexCoords = 4;

	// enable to texture binding
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, m_textureObj);

	if (newImage)
	{
		// if the size changed, then reset the texture
		if (m_pixBlock.image.csize != m_dataSize[0] ||
			m_pixBlock.image.xsize != m_dataSize[1] ||
			m_pixBlock.image.ysize != m_dataSize[2])
		{
			m_dataSize[0] = m_pixBlock.image.csize;
			m_dataSize[1] = m_pixBlock.image.xsize;
			m_dataSize[2] = m_pixBlock.image.ysize;
		
			glTexImage2D(GL_TEXTURE_2D, 0,
					m_pixBlock.image.csize,
	    			m_pixBlock.image.xsize,
	    			m_pixBlock.image.ysize, 0,
	    			m_pixBlock.image.format,
    				m_pixBlock.image.type,
    				m_pixBlock.image.data);
		}

		// okay, load in the actual pixel data
		glTexSubImage2D(GL_TEXTURE_2D, 0,
   			0, 0,							// position
   			m_xsize,						// the x size of the data
   			m_ysize,						// the y size of the data
   			m_pixBlock.image.format,		// the format
			m_pixBlock.image.type,			// the type
			pt + 40);						// the data + header offset
	}
}

/////////////////////////////////////////////////////////
// postrender
//
/////////////////////////////////////////////////////////
void pix_movie :: postrender(GemState *state)
{
    m_pixBlock.newimage = 0;
    state->image = NULL;
	state->texture = 0;

	glDisable(GL_TEXTURE_2D);	
}

/////////////////////////////////////////////////////////
// startRendering
//
/////////////////////////////////////////////////////////
void pix_movie :: startRendering()
{
    glGenTextures(1, &m_textureObj);
    glBindTexture(GL_TEXTURE_2D, m_textureObj);
	setUpTextureState();

    m_pixBlock.newimage = 1;
	m_dataSize[0] = m_dataSize[1] = m_dataSize[2] = 0;
}

/////////////////////////////////////////////////////////
// stopRendering
//
/////////////////////////////////////////////////////////
void pix_movie :: stopRendering()
{
    if (m_textureObj)
		glDeleteTextures(1, &m_textureObj);
    m_textureObj = 0;
	m_dataSize[0] = m_dataSize[1] = m_dataSize[2] = 0;
}

/////////////////////////////////////////////////////////
// setUpTextureState
//
/////////////////////////////////////////////////////////
void pix_movie :: setUpTextureState()
{
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
}

/////////////////////////////////////////////////////////
// changeImage
//
/////////////////////////////////////////////////////////
void pix_movie :: changeImage(int imgNum)
{
    if (imgNum >= m_numFrames)
    {
    	error("GEM: pix_movie: selection number too high: %d (max num is %d)", imgNum, m_numFrames);
    	return;
    }
    else if (imgNum < 0)
    {
        error("GEM: pix_movie: selection number must be > 0");
        return;
    }
    m_reqFrame = imgNum;
}

/////////////////////////////////////////////////////////
// static member function
//
/////////////////////////////////////////////////////////
void pix_movie :: obj_setupCallback(t_class *classPtr)
{
    class_addmethod(classPtr, (t_method)&pix_movie::openMessCallback,
    	    gensym("open"), A_SYMBOL, A_NULL);
    class_addmethod(classPtr, (t_method)&pix_movie::changeImageCallback,
    	    gensym("img_num"), A_FLOAT, A_NULL);
}
void pix_movie :: openMessCallback(void *data, t_symbol *filename)
{
    GetMyClass(data)->openMess(filename);
}
void pix_movie :: changeImageCallback(void *data, t_floatarg imgNum)
{
    GetMyClass(data)->changeImage((int)imgNum);
}
