//
// PNGimp.cpp
//
// PNG Import Plugin
//
// Copyright (c) 2000 memon/moppi productions
//

//#define WIN32_LEAN_AND_MEAN		// Exclude rarely-used stuff from Windows headers
#include <windows.h>
#include <gl\gl.h>
#include <gl\glu.h>

// Demopaja headers
#include "DemopajaVersion.h"
#include "PajaTypes.h"
#include "ClassDescC.h"
#include "ImportableI.h"
#include "OpenGLDeviceC.h"
#include "OpenGLViewportC.h"
#include "pngimp.h"
#include "FileIO.h"
#include "ImportableImageI.h"
#include "ImageResampleC.h"

#include "png.h"
#include "minilzo.h"

using namespace PajaTypes;
using namespace PluginClass;
using namespace PajaSystem;
using namespace Edit;
using namespace Composition;
using namespace Import;
using namespace FileIO;

using namespace PNGPlugin;


static
void
TRACE( const char* szFormat, ...  )
{
	char	szMsg[256];
	va_list	rList;
	va_start( rList, szFormat );
	_vsnprintf( szMsg, 255, szFormat, rList );
	va_end( rList );
	OutputDebugString( szMsg );
}




inline
uint32
lowest_bit_mask( uint32 v )
{
	return (v & -v);
}

static
uint32
ceil_power2( uint32 ui32Num )
{
	uint32	i = lowest_bit_mask( ui32Num );
	while( i < ui32Num )
		i <<= 1;
	return i;
}

static
int32
nearest_power2( int32 i32Num )
{
	int32	i = ceil_power2( i32Num );	// bigger
	int32	j = i / 2;					// smaller

	int32	i32DiffI = i - i32Num;
	int32	i32DiffJ = i32Num - j;

	// diff J has to be twice as little as diffI to be chosen.
	i32DiffI /= 2;

	if( i32DiffJ < i32DiffI )
		return j;

	return i;
}


//////////////////////////////////////////////////////////////////////////
//
//  PNG importer class descriptor.
//

PNGImportDescC::PNGImportDescC()
{
	// empty
}

PNGImportDescC::~PNGImportDescC()
{
	// empty
}

void*
PNGImportDescC::create()
{
	return PNGImportC::create_new();
}

int32
PNGImportDescC::get_classtype() const
{
	return CLASS_TYPE_FILEIMPORT;
}

SuperClassIdC
PNGImportDescC::get_super_class_id() const
{
	return SUPERCLASS_IMAGE;
}

ClassIdC
PNGImportDescC::get_class_id() const
{
	return CLASS_PNG_IMPORT;
}

const char*
PNGImportDescC::get_name() const
{
	return "PNG Image";
}

const char*
PNGImportDescC::get_desc() const
{
	return "Importer for Portable Network Graphics (PNG) images";
}

const char*
PNGImportDescC::get_author_name() const
{
	return "Mikko \"memon\" Mononen";
}

const char*
PNGImportDescC::get_copyright_message() const
{
	return "Copyright (c) 2000 Moppi Productions";
}

const char*
PNGImportDescC::get_url() const
{
	return "http://moppi.inside.org/demopaja/";
}

const char*
PNGImportDescC::get_help_filename() const
{
	return "res://PNGhelp.html";
}

uint32
PNGImportDescC::get_required_device_driver_count() const
{
	return 1;
}

const ClassIdC&
PNGImportDescC::get_required_device_driver( uint32 ui32Idx )
{
	return PajaSystem::CLASS_OPENGL_DEVICEDRIVER;
}

uint32
PNGImportDescC::get_ext_count() const
{
	return 1;
}

const char*
PNGImportDescC::get_ext( uint32 ui32Index ) const
{
	if( ui32Index == 0 )
		return "PNG";
	return 0;
}



PNGImportDescC	g_rPNGImportDesc;

#ifndef DEMOPAJAPLAYER

//////////////////////////////////////////////////////////////////////////
//
// The DLL
//

BOOL APIENTRY
DllMain( HANDLE hModule, DWORD ulReasonForCall, LPVOID lpReserved )
{
    switch( ulReasonForCall )
	{
		case DLL_PROCESS_ATTACH:
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
		case DLL_PROCESS_DETACH:
			break;
    }
    return TRUE;
}


//
// Returns number of classes inside this plugin DLL.
//

__declspec( dllexport )
int32
get_classdesc_count()
{
	return 1;
}


//
// Returns class descriptors of the plugin classes.
//

__declspec( dllexport )
ClassDescC*
get_classdesc( int32 i )
{
	if( i == 0 )
		return &g_rPNGImportDesc;
	return 0;
}


//
// Returns the API version this DLL was made with.
//

__declspec( dllexport )
int32
get_api_version()
{
	return DEMOPAJA_VERSION;
}

//
// Returns the DLL name.
//

__declspec( dllexport )
char*
get_dll_name()
{
	return "PNGimp.dll - PNG Image Plugin (c)2000 memon/moppi productions";
}

#endif




//////////////////////////////////////////////////////////////////////////
//
// PNG Importer class implementation.
//

PNGImportC::PNGImportC() :
	m_ui32TextureId( 0 ),
	m_pData( 0 ),
	m_i32Width( 0 ),
	m_i32Height( 0 ),
	m_i32Bpp( 0 )
{
	m_rTexBounds[0] = Vector2C( 0, 0 );
	m_rTexBounds[1] = Vector2C( 1, 1 );
}

PNGImportC::PNGImportC( EditableI* pOriginal ) :
	ImportableImageI( pOriginal ),
	m_ui32TextureId( 0 ),
	m_pData( 0 ),
	m_i32Width( 0 ),
	m_i32Height( 0 ),
	m_i32Bpp( 0 )
{
	m_rTexBounds[0] = Vector2C( 0, 0 );
	m_rTexBounds[1] = Vector2C( 1, 1 );
}

PNGImportC::~PNGImportC()
{
	// Return if this is a clone.
	if( get_original() )
		return;

	// Delete creted texture.
	if( m_ui32TextureId )
		glDeleteTextures( 1, &m_ui32TextureId );

	delete [] m_pData;
}

PNGImportC*
PNGImportC::create_new()
{
	return new PNGImportC;
}

DataBlockI*
PNGImportC::create()
{
	return new PNGImportC;
}

DataBlockI*
PNGImportC::create( EditableI* pOriginal )
{
	return new PNGImportC( pOriginal );
}

void
PNGImportC::copy( EditableI* pEditable )
{
	// Importables which loads the data from a file does
	// not have to implement this method, since they will
	// not be duplicated.
}

void
PNGImportC::restore( EditableI* pEditable )
{
	PNGImportC*	pFile = (PNGImportC*)pEditable;

	m_ui32TextureId = pFile->m_ui32TextureId;
	m_pData = pFile->m_pData;
	m_i32Width = pFile->m_i32Width;
	m_i32Height = pFile->m_i32Height;
	m_i32Bpp = pFile->m_i32Bpp;
	m_sFileName = pFile->m_sFileName;
}

const char*
PNGImportC::get_filename()
{
	return m_sFileName.c_str();
}

void
PNGImportC::set_filename( const char* szName )
{
	m_sFileName = szName;
}


void
PNG_warning_function( png_structp png_ptr, png_const_charp error )
{
	// no warnings
}


void
PNG_error_function( png_structp png_ptr, png_const_charp warning ) {
	// copied from libpng's pngerror.cpp, but without the fprintf
	jmp_buf	jmpbuf;
	memcpy( jmpbuf, png_ptr->jmpbuf, sizeof( jmp_buf ) );
	longjmp( jmpbuf, 1 );
}


bool
PNGImportC::load_file( const char* szName, DemoInterfaceC* pInterface )
{
	// Store interface pointer.
	m_pDemoInterface = pInterface;

	FILE*	fp = fopen( pInterface->get_absolute_path( szName ), "rb" );
	if( !fp ) {
		return false;
	}

	png_byte	header[8];
	const int32	header_size = 8;

	fread( header, 1, header_size, fp );
	int32	is_png = !png_sig_cmp( header, 0, header_size );
	if( !is_png ) {
		TRACE( "PNG: false signature\n" );
		return false;
	}	

	png_structp	png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, NULL, PNG_error_function, PNG_warning_function );
	if( !png_ptr ) {
		TRACE( "PNG: png_create_read_struct()\n" );
		return false;
	}
	
	png_infop	info_ptr = png_create_info_struct( png_ptr );
	if( !info_ptr ) {
		TRACE( "PNG: png_create_info_struct()\n" );
		png_destroy_read_struct( &png_ptr, (png_infopp)NULL, (png_infopp)NULL );
		return false;
	}
	
	if( setjmp( png_jmpbuf( png_ptr ) ) ) {
		png_destroy_read_struct( &png_ptr, &info_ptr, NULL );
		fclose( fp );
		return false;
	}

	png_init_io( png_ptr, fp );
	png_set_sig_bytes( png_ptr, header_size );

	int png_transform = PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_EXPAND;
	png_read_png( png_ptr, info_ptr, png_transform, NULL );

	int w = png_get_image_width( png_ptr, info_ptr );
	int h = png_get_image_height( png_ptr, info_ptr );

	// decode based on pixel format
	int bit_depth = png_get_bit_depth( png_ptr, info_ptr );
	int channels = png_get_channels( png_ptr, info_ptr );
	png_bytepp row_pointers = png_get_rows( png_ptr, info_ptr );

	int32	i, j;

	if( channels == 1 ) {
		// alpha or grey
		m_i32Bpp = 8;
		m_i32Width = w;
		m_i32Height = h;
		m_pData = new uint8[w * h];
		uint8*	pDst = m_pData;

		for( i = 0; i < h; i++ ) {
			uint8*	pSrc = row_pointers[h - i - 1];
			for( j = 0; j < w; j++ ) {
//				*pDst++ = img[((h - i - 1) * row_bytes + j)];
				*pDst++ = *pSrc++;
			}
		}
	}
	else if( channels == 2 ) {
		// alpha gery --> RGBA
		m_i32Bpp = 32;
		m_i32Width = w;
		m_i32Height = h;
		m_pData = new uint8[w * h * 4];
		uint8*	pDst = m_pData;

		for( i = 0; i < h; i++ ) {
			uint8*	pSrc = row_pointers[h - i - 1];
			for( j = 0; j < w; j++ ) {
				*pDst++ = *pSrc;
				*pDst++ = *pSrc;
				*pDst++ = *pSrc;
				pSrc++;
				*pDst++ = *pSrc++;
			}
		}
	}
	else if( channels == 3 ) {
		// RGB
		m_i32Bpp = 24;
		m_i32Width = w;
		m_i32Height = h;
		m_pData = new uint8[w * h * 3];
		uint8*	pDst = m_pData;

		for( i = 0; i < h; i++ ) {
			uint8*	pSrc = row_pointers[h - i - 1];
			for( j = 0; j < w; j++ ) {
				*pDst++ = *pSrc++;
				*pDst++ = *pSrc++;
				*pDst++ = *pSrc++;
			}
		}
	}
	else if( channels == 4 ) {
		// RGBA
		m_i32Bpp = 32;
		m_i32Width = w;
		m_i32Height = h;
		m_pData = new uint8[w * h * 4];
		uint8*	pDst = m_pData;

		for( i = 0; i < h; i++ ) {
			uint8*	pSrc = row_pointers[h - i - 1];
			for( j = 0; j < w; j++ ) {
				*pDst++ = *pSrc++;
				*pDst++ = *pSrc++;
				*pDst++ = *pSrc++;
				*pDst++ = *pSrc++;
			}
		}
	}
	else
	{
		png_destroy_read_struct( &png_ptr, &info_ptr, NULL );
		fclose( fp );
		TRACE( "Weird PNG format\n" );
		return false;
	}


	png_destroy_read_struct( &png_ptr, &info_ptr, NULL );

	fclose( fp );

	// store filename
	m_sFileName = szName;

	return true;
}

void
PNGImportC::initialize( uint32 ui32Reason, DemoInterfaceC* pInterface )
{
	ImportableI::initialize( ui32Reason, pInterface );

	DeviceContextC* pContext = m_pDemoInterface->get_device_context();
	TimeContextC* pTimeContext = m_pDemoInterface->get_time_context();

	if( ui32Reason == INIT_DEVICE_CHANGED ) {

		OpenGLDeviceC*	pDevice = (OpenGLDeviceC*)pContext->query_interface( CLASS_OPENGL_DEVICEDRIVER );
		if( !pDevice )
			return;

		if( pDevice->get_state() == DEVICE_STATE_SHUTTINGDOWN ) {
			// Delete textures
			glDeleteTextures( 1, &m_ui32TextureId );
			m_ui32TextureId = 0;
		}

	}
	else if( ui32Reason == INIT_INITIAL_UPDATE ) {

		OpenGLDeviceC*	pDevice = (OpenGLDeviceC*)pContext->query_interface( CLASS_OPENGL_DEVICEDRIVER );
		if( !pDevice )
			return;

		if( !m_ui32TextureId )
			upload_texture();
	}
}

ClassIdC
PNGImportC::get_class_id()
{
	return CLASS_PNG_IMPORT;
}

const char*
PNGImportC::get_class_name()
{
	return "PNG Image";
}

float32
PNGImportC::get_width()
{
	return (float32)m_i32Width;
}

float32
PNGImportC::get_height()
{
	return (float32)m_i32Height;
}

int32
PNGImportC::get_data_width()
{
	return m_i32Width;
}

int32
PNGImportC::get_data_height()
{
	return m_i32Height;
}

int32
PNGImportC::get_data_pitch()
{
	return m_i32Width;
}

BBox2C&
PNGImportC::get_tex_coord_bounds()
{
	return m_rTexBounds;
}

int32
PNGImportC::get_data_bpp()
{
	return m_i32Bpp;
}

uint8*
PNGImportC::get_data()
{
	return m_pData;
}


void
PNGImportC::upload_texture()
{
	glGenTextures( 1, &m_ui32TextureId );
	glBindTexture( GL_TEXTURE_2D, m_ui32TextureId );
	glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );

	uint8*	pResampleData = 0;

	int32	i32Width = m_i32Width;
	int32	i32Height = m_i32Height;
	uint8*	pData = m_pData;

	int32	i32WidthPow2 = nearest_power2( i32Width );
	int32	i32HeightPow2 = nearest_power2( i32Height );

	if( i32WidthPow2 != m_i32Width || i32HeightPow2 != m_i32Height ) {
		pResampleData = new uint8[i32WidthPow2 * i32HeightPow2 * (m_i32Bpp / 8)];
		ImageResampleC::resample_bilinear( m_i32Bpp, m_pData, m_i32Width, m_i32Height, pResampleData, i32WidthPow2, i32HeightPow2 );
		pData = pResampleData;
		i32Width = i32WidthPow2;
		i32Height = i32HeightPow2;
	}

	if( m_i32Bpp == 8 ) {
		glTexImage2D( GL_TEXTURE_2D, 0, GL_ALPHA8, i32Width, i32Height, 0, GL_ALPHA , GL_UNSIGNED_BYTE, pData );
	}
	else if( m_i32Bpp == 24 ) {
		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, i32Width, i32Height, 0, GL_RGB, GL_UNSIGNED_BYTE, pData );
	}
	else if( m_i32Bpp ==32 ) {
		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, i32Width, i32Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pData );
	}

	if( pResampleData )
		delete [] pResampleData;
}


void
PNGImportC::bind_texture( DeviceInterfaceI* pInterface, uint32 ui32Stage, uint32 ui32Properties )
{
	if( pInterface->get_class_id() != CLASS_OPENGL_DEVICEDRIVER )
		return;

	if( !m_ui32TextureId ) {
		upload_texture();
	}
	else
		glBindTexture( GL_TEXTURE_2D, m_ui32TextureId );

	if( ui32Properties & IMAGE_LINEAR ) {
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
	}
	else {
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
	}

	if( ui32Properties & IMAGE_CLAMP ) {
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );
	}
	else {
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
	}

}

const char*
PNGImportC::get_info()
{
	static char	szInfo[256];
	char*	szType;
	if( m_i32Bpp == 8 )
		szType = "ALPHA";
	else if( m_i32Bpp == 16 )
		szType = "RGB";
	else if( m_i32Bpp == 24 )
		szType = "RGB";
	else if( m_i32Bpp == 32 )
		szType = "RGBA";

	_snprintf( szInfo, 255, "%s %d x %d x %dbit", szType, m_i32Width, m_i32Height, m_i32Bpp );
	return szInfo;
}

ClassIdC
PNGImportC::get_default_effect()
{
	const	PluginClass::ClassIdC	CLASS_IMAGE_EFFECT( 0, 100 );
	return CLASS_IMAGE_EFFECT;
}


int32
PNGImportC::get_duration()
{
	return -1;
}

float32
PNGImportC::get_start_label()
{
	return 0;
}

float32
PNGImportC::get_end_label()
{
	return 0;
}

void
PNGImportC::eval_state( int32 i32Time )
{
	// empty
}


enum PNGImportChunksE {
	CHUNK_PNGIMPORT_BASE =		0x1000,
	CHUNK_PNGIMPORT_DATA =		0x2000,
};

const uint32	PNGIMPORT_VERSION = 1;


uint32
PNGImportC::save( SaveC* pSave )
{
	uint32		ui32Error = IO_OK;
	std::string	sStr;
	uint8		ui8Tmp;

	// file base
	pSave->begin_chunk( CHUNK_PNGIMPORT_BASE, PNGIMPORT_VERSION );
		sStr = m_sFileName;
		if( sStr.size() > 255 )
			sStr.resize( 255 );
		ui32Error = pSave->write_str( sStr.c_str() );
	pSave->end_chunk();

	// file data
	pSave->begin_chunk( CHUNK_PNGIMPORT_DATA, PNGIMPORT_VERSION );
		ui32Error = pSave->write( &m_i32Width, sizeof( m_i32Width ) );
		ui32Error = pSave->write( &m_i32Height, sizeof( m_i32Height ) );
		ui8Tmp = (uint8)m_i32Bpp;
		ui32Error = pSave->write( &ui8Tmp, sizeof( ui8Tmp ) );

		//
		// compress
		//
		int32	i32DataSize = m_i32Width * m_i32Height * (m_i32Bpp / 8);
		uint32	ui32CompressedSize;
		uint8*	pCompressed = new uint8[i32DataSize + i32DataSize / 64 + 16 + 3];
		long*	pWorkmem = new long[((LZO1X_1_MEM_COMPRESS) + (sizeof(long) - 1)) / sizeof(long)];

		if( lzo1x_1_compress( m_pData, i32DataSize, pCompressed, &ui32CompressedSize, pWorkmem ) != LZO_E_OK )
			ui32Error = IO_ERROR_WRITE;

		// write compressed data size
		ui32Error = pSave->write( &ui32CompressedSize, sizeof( ui32CompressedSize ) );

		// write compressed data
		ui32Error = pSave->write( pCompressed, ui32CompressedSize );

		delete [] pWorkmem;
		delete [] pCompressed;
	pSave->end_chunk();

	return ui32Error;
}

uint32
PNGImportC::load( LoadC* pLoad )
{
	uint32	ui32Error = IO_OK;
	char	szStr[256];
	uint8	ui8Tmp;

	while( (ui32Error = pLoad->open_chunk()) == IO_OK ) {

		switch( pLoad->get_chunk_id() ) {
		case CHUNK_PNGIMPORT_BASE:
			{
				if( pLoad->get_chunk_version() == PNGIMPORT_VERSION ) {
					ui32Error = pLoad->read_str( szStr );
					m_sFileName = szStr;
				}
			}
			break;

		case CHUNK_PNGIMPORT_DATA:
			{
				if( pLoad->get_chunk_version() == PNGIMPORT_VERSION ) {
					ui32Error = pLoad->read( &m_i32Width, sizeof( m_i32Width ) );
					ui32Error = pLoad->read( &m_i32Height, sizeof( m_i32Height ) );
					ui32Error = pLoad->read( &ui8Tmp, sizeof( ui8Tmp ) );
					m_i32Bpp = ui8Tmp;
					// delete old data if any
					delete m_pData;


					//
					// decompress
					//

					uint32	ui32DataSize = m_i32Width * m_i32Height * (m_i32Bpp / 8);
					uint32	ui32DecompressedSize;
					m_pData = new uint8[ui32DataSize];
					uint32	ui32CompressedSize;

					// read compressed data size
					ui32Error = pLoad->read( &ui32CompressedSize, sizeof( ui32CompressedSize ) );

					uint8*	pCompressed = new uint8[ui32CompressedSize];

					// write compressed data
					ui32Error = pLoad->read( pCompressed, ui32CompressedSize );

					if( lzo1x_decompress( pCompressed, ui32CompressedSize, m_pData, &ui32DecompressedSize, NULL ) != LZO_E_OK )
						ui32Error = IO_ERROR_READ;

					if( ui32DecompressedSize != ui32DataSize )
						OutputDebugString( "data size mismatch\n" );

					delete [] pCompressed;
					
				}
			}
			break;

		default:
			assert( 0 );
		}

		pLoad->close_chunk();

		if( ui32Error != IO_OK && ui32Error != IO_END ) {
			return ui32Error;
		}
	}

	return ui32Error;
}


