/* (c) 1999-2000 Tino Schwarze, see COPYING for details */
/**
 * implementation of class cTextureMaterial
 */

#include <GL/glu.h>

#include <sys/types.h>
#include <stdio.h>	// get FILE operations (used for jpeg reading)

extern "C" {	// libjpeg is not prepared for C++ use... :-(
#include <jpeglib.h>
}

#include "cTextureMaterial.hh"

/**
 * default constructor
 	*/
cTextureMaterial::cTextureMaterial (const char *name)
:	cMaterial (name)
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::cTextureMaterial ()");
	SetTextureParameters ();
}

/**
 * constructor with material initialization
 	*/
cTextureMaterial::cTextureMaterial (
	const cADSEColor &color,
	const int shininess,
	const char *name)
:	cMaterial (color, shininess, name)
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::cTextureMaterial ()");
	SetTextureParameters ();
}

/**
 * constructor with material initialization
 	*/
cTextureMaterial::cTextureMaterial (
	const char *texture,
	const cADSEColor &color,
	const int shininess,
	const char *name)
:	cMaterial (color, shininess, name),
	mTextureFile (texture),
	mTextureData (NULL)
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::cTextureMaterial ()");
	SetTextureParameters ();
}

/**
 * constructor with material initialization
 	*/
cTextureMaterial::cTextureMaterial (
	const char *texture,
	bool usemipmaps,
	const cADSEColor &color,
	const int shininess,
	const char *name)
:	cMaterial (color, shininess, name),
	mTextureFile (texture),
	mMipmapped (usemipmaps),
	mTextureData (NULL)
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::cTextureMaterial ()");
	SetTextureParameters ();
}

/**
 * copy-constructor
 	*/
cTextureMaterial::cTextureMaterial (const cTextureMaterial &ctm)
:	cMaterial ((const cMaterial &)ctm),
	mTextureFile (ctm.mTextureFile),
	mMipmapped (ctm.mMipmapped),
	mSizeX (ctm.mSizeX),
	mSizeY (ctm.mSizeY),
	mComponentNo (ctm.mComponentNo),
	mMinFilter (ctm.mMinFilter),
	mMagFilter (ctm.mMagFilter),
	mWrapS (ctm.mWrapS),
	mWrapT (ctm.mWrapT),
	mEnvMode (ctm.mEnvMode)
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::cTextureMaterial (const cTextureMaterial &)");
	// prevent using the same texture name twice!
	// (UpdateTexture() will allocate a new one later)
	mTextureName = 0;

	// copy texture data - calls UpdateTexture()
	UseTexture (ctm.mSizeX, ctm.mSizeY, ctm.mComponentNo, 
		(unsigned char *)ctm.mTextureData, ctm.mMipmapped);

}

/**
 * destructor
 	*/
cTextureMaterial::~cTextureMaterial ()
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::~cTextureMaterial ()");

	if (mTextureName != 0)
	{
		glDeleteTextures (1, &mTextureName);
	}

	delete mTextureData;
}


/**
 * initialization
 	*/
int cTextureMaterial::Init ()
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::Init ()");

	mInitialized = true;

	AllocateTextureName ();

	// texture has been specified but not yet loaded?
	if ((! mTextureFile.empty()) && (mTextureData == NULL))
	{

		if (LoadTexture (mTextureFile.c_str(), mMipmapped) != 0)
		{
			return -1;
		}

	}
	else
	{

		if (mTextureData != NULL)
		{
			UpdateTexture ();
		}

	}

	return cMaterial::Init ();
}

void cTextureMaterial::SetMipmapping (bool state)
{
	mMipmapped = state;
	UpdateTexture ();
}

bool cTextureMaterial::IsMipmapped () const
{
	return mMipmapped;
}

/**
 * enable material's properties
 	*/
void cTextureMaterial::StartMaterialUse ()
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::StartMaterialUse ()");

	GLenum gl_err;

	cMaterial::StartMaterialUse ();

	// do we have a texture?
	if ((mTextureName != 0) && (mTextureData != NULL))
	{
		// get actual state of texturing
		mOldTex2DState = glIsEnabled (GL_TEXTURE_2D);

		if ((gl_err = glGetError ()) != GL_NO_ERROR) { cerr << "OpenGL error: " << gluErrorString (gl_err) << endl; }

		if (mOldTex2DState == GL_FALSE)
		{
			glEnable (GL_TEXTURE_2D);
		}

		// get actual texture binding
		glGetIntegerv (GL_TEXTURE_BINDING_2D, (GLint *)&mOldTexBinding);
		if ((gl_err = glGetError ()) != GL_NO_ERROR) { cerr << "OpenGL error: " << gluErrorString (gl_err) << endl; }
		glBindTexture (GL_TEXTURE_2D, mTextureName);
		if ((gl_err = glGetError ()) != GL_NO_ERROR) { cerr << "OpenGL error: " << gluErrorString (gl_err) << endl; }
/*
		glGetIntegerv (GL_TEXTURE_BINDING_2D, (GLint *)&old_state);
		if ((gl_err = glGetError ()) != GL_NO_ERROR) { cerr << "OpenGL error: " << gluErrorString (gl_err) << endl; }
*/

#if 0
		// We need different filters for mipmapped and non-mipmapped
		// textures.
		const GLint min_filter = mMipmapped ? mMinMmFilter : mMinFilter;

		glGetTexParameteriv (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &mOldMinFilter);
		if (mOldMinFilter != min_filter)
		{
			glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
		}

		glGetTexParameteriv (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &mOldMagFilter);
		if (mOldMagFilter != mMagFilter)
		{
			glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mMagFilter);
		}

		glGetTexParameteriv (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, &mOldWrapS);
		if (mOldWrapS != mWrapS)
		{
			glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, mWrapS);
		}

		glGetTexParameteriv (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, &mOldWrapT);
		if (mOldWrapT != mWrapT)
		{
			glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, mWrapT);
		}
#endif

		glGetTexEnviv (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, &mOldEnvMode);
		if (mEnvMode != mOldEnvMode)
		{
			glTexEnvi (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, mEnvMode);
		}

		if ((gl_err = glGetError ()) != GL_NO_ERROR) { cerr << "OpenGL error: " << gluErrorString (gl_err) << endl; }

		//UpdateTexture ();
	}

}

/**
 * disable materials properties (restore original settings)
 	*/
void cTextureMaterial::EndMaterialUse ()
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::EndMaterialUse ()");

	cMaterial::EndMaterialUse ();

#if 0
	// We need different filters for mipmapped and non-mipmapped
	// textures.
	const GLint min_filter = mMipmapped ? mMinMmFilter : mMinFilter;

	// restore old texture parameters
	if (mOldMinFilter != min_filter)
	{
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mOldMinFilter);
	}
	if (mOldMagFilter != mMagFilter)
	{
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mOldMagFilter);
	}
	if (mOldWrapS != mWrapS)
	{
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, mOldWrapS);
	}
	if (mOldWrapT != mWrapT)
	{
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, mOldWrapT);
	}
#endif

	if (mEnvMode != mOldEnvMode)
	{
		glTexEnvi (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, mOldEnvMode);
	}

	// we might not have a texture at all
	if ((mTextureName != 0) && (mTextureData != NULL))
	{
		glBindTexture (GL_TEXTURE_2D, mOldTexBinding);
	}

	// disable texturing if it was disabled before StartMaterialUse
	if (mOldTex2DState == GL_FALSE)
	{
		glDisable (GL_TEXTURE_2D);
	}

}


/**
 * load specified texture
 	*/
int cTextureMaterial::LoadTexture (
	const char *texture,
	bool usemipmaps)
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::LoadTexture ()");

	// release old texture (if any)
	delete mTextureData;

	if (texture == NULL)	// no new texture wanted?
	{
		if (mTextureName != 0)
		{
			// release texture from OpenGL
			glDeleteTextures (1, &mTextureName);
			mTextureName = 0;
		}

		return 0;
	}

	mMipmapped = usemipmaps;

	mTextureFile = texture;

	// now load the texture
	struct jpeg_decompress_struct cinfo;
	FILE *infile;
	JSAMPROW next_row;	// *JSAMPLE
	JSAMPARRAY buffer = &next_row;	// *JSAMPROW => **JSAMPLE
	int row_size;
	struct jpeg_error_mgr jerr;
	
	if ((infile = fopen (texture, "rb")) == NULL)
	{
		cerr << "error opening " << texture << " for reading." << endl;
		return -1;
	}

	cinfo.err = jpeg_std_error (&jerr);
	jpeg_create_decompress (&cinfo);
	jpeg_stdio_src (&cinfo, infile);
	jpeg_read_header (&cinfo, TRUE);
	cinfo.dct_method = JDCT_FLOAT;	// best quality
	cinfo.out_color_space = JCS_RGB;	// we want RGB output
	jpeg_start_decompress (&cinfo);

	row_size = cinfo.output_width * cinfo.output_components;
	mSizeX = cinfo.output_width;
	mSizeY = cinfo.output_height;

	// aquire space for uncompressed texture
	mTextureData = (void *)new char[mSizeY * row_size];

	if (mTextureData == NULL)
	{
		cerr << "error allocating " << mSizeY * row_size << " bytes for texture" << endl;
		fclose (infile);
		return -1;
	}

	next_row = (JSAMPROW)mTextureData;

	// now start reading the JPEG
	while (cinfo.output_scanline < cinfo.output_height) 
	{
		// read one scanline at a time
		jpeg_read_scanlines(&cinfo, buffer, 1);
		next_row += row_size;
	}

	// yip. JPEG read in - release all used memory
	jpeg_finish_decompress (&cinfo);
	jpeg_destroy_decompress (&cinfo);
	fclose (infile);

/*
	FILE *outfile = fopen ("tex.raw", "wb");
	fwrite (mTextureData, 1, mSizeY * row_size, outfile);
	fclose (outfile);
*/

	mComponentNo = 3;

	UpdateTexture ();

	return 0;
}


/* use user-specified texture */
int cTextureMaterial::UseTexture (
	int width,
	int height,
	int components,
	const unsigned char *data,
	bool usemipmaps)
{
	if (data == NULL)	// no new texture wanted?
	{
		if (mTextureName != 0)
		{
			// release texture from OpenGL
			glDeleteTextures (1, &mTextureName);
			mTextureName = 0;
		}

		// deleting a texture should succeed...
		return 0;
	}

	if ((components != 3) && (components != 4))
	{
		return -1;
	}

	mTextureFile = "";

	mComponentNo = components;

	delete mTextureData;

	mTextureData = (void *)new unsigned char[width*height*components];

	memcpy (mTextureData, data, width*height*components);

	mSizeX = width;
	mSizeY = height;
	mMipmapped = usemipmaps;

	UpdateTexture ();

	return 0;
}


/**
 * bring our texture to OpenGL
 * (texture has to have been loaded)
 */
void cTextureMaterial::UpdateTexture ()
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::UpdateTexture ()");

	if (! mInitialized)
	{
		return;
	}

	// aquire texture "name" (= handle) if neccessary
	if (mTextureName == 0)
	{
		AllocateTextureName ();
	}

	// do we have a texture loaded?
	if ((mTextureName != 0) && (mTextureData != NULL))
	{
		GLint old_texbind;
		GLenum gl_err;

		// save actual texture binding
		glGetIntegerv (GL_TEXTURE_BINDING_2D, &old_texbind);
		if ((gl_err = glGetError ()) != GL_NO_ERROR) { cerr << "OpenGL error: " << gluErrorString (gl_err) << endl; }

		// first, bind our texture name to current texture, so we're able to
		// modify it here
		glBindTexture (GL_TEXTURE_2D, mTextureName);

		if (mMipmapped)
		{
			gluBuild2DMipmaps (
					GL_TEXTURE_2D, 
					mComponentNo, mSizeX, mSizeY, 
					(mComponentNo == 3) ? GL_RGB : GL_RGBA,
					GL_UNSIGNED_BYTE, mTextureData);
		}
		else
		{
			// no mipmapping - load level 0 texture
			glTexImage2D (
					GL_TEXTURE_2D, 
					0, // mipmap level 0
					(mComponentNo == 3) ? GL_RGB : GL_RGBA,
					mSizeX, mSizeY, 
					0, // no border
					(mComponentNo == 3) ? GL_RGB : GL_RGBA,
					GL_UNSIGNED_BYTE,
					mTextureData);
		}

		gl_err = glGetError ();

		if (gl_err != GL_NO_ERROR)
		{
			cerr << "OpenGL error occured:" << gluErrorString (gl_err) << endl;
		}

		// different filters for mipmapped and non-mipmapped texture
		const GLint min_filter = mMipmapped ? mMinMmFilter : mMinFilter;

		// set texture parameters
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mMagFilter);
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, mWrapS);
		glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, mWrapT);

		gl_err = glGetError ();

		if (gl_err != GL_NO_ERROR)
		{
			cerr << "OpenGL error occured:" << gluErrorString (gl_err) << endl;
		}

		// restore old binding
		glBindTexture (GL_TEXTURE_2D, old_texbind);
	}

}


/**
 * set texturing parameters to use
 	*/
void cTextureMaterial::SetTextureParameters (
	GLenum min_filter,
	GLenum min_mm_filter,
	GLenum mag_filter,
	GLenum wrap_s,
	GLenum wrap_t,
	GLenum env_mode)
{
	mMinFilter = min_filter;
	mMinMmFilter = min_mm_filter;
	mMagFilter = mag_filter;
	mWrapS = wrap_s;
	mWrapT = wrap_t;
	mEnvMode = env_mode;

	// activate changes
	UpdateTexture ();

}


/**
 * activation function
 	*/
void cTextureMaterial::Activate ()
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::Activate ()");

	StartMaterialUse ();

	// activate all childs
	// note: we _do not_ call cMaterial::Activate() since it would
	// call StartMaterialUse() again and mess things up
	cObject::Activate ();
}

/**
 * deactivation function
 	*/
void cTextureMaterial::Deactivate ()
{
	ENTER_OBJECT_METHOD ("cTextureMaterial::Deativate ()");

	EndMaterialUse();

	cObject::Deactivate ();
}

void cTextureMaterial::AllocateTextureName ()
{
	/* do not allocate a new texture name if we've already got one */
	if (mTextureName == 0)
	{
		glGenTextures (1, &mTextureName);

		GLenum gl_err = glGetError ();

		if (gl_err != GL_NO_ERROR)
		{
			cerr << "OpenGL error occured:" << gluErrorString (gl_err) << endl;
		}
	}

}

