#ifndef IMAGE_INCLUDED
#define IMAGE_INCLUDED

#define SUPPORT_TILES

#include <string.h>
#include "MyMiscellany.h"

struct ImageReader
{
	virtual unsigned int nextRow( unsigned char* row ) = 0;
	static unsigned char* Read( const char* fileName , unsigned int& width , unsigned int& height , unsigned int& channels )
	{
		ImageReader* reader = Get( fileName );
		width = reader->width() , height = reader->height() , channels = reader->channels();
		unsigned char* pixels = new unsigned char[ width*height*channels ];
		for( unsigned int j=0 ; j<height ; j++ ) reader->nextRow( pixels + j*width*channels );
		delete reader;
		return pixels;
	}
	static unsigned char* ReadColor( const char* fileName , unsigned int& width , unsigned int& height )
	{
		unsigned int channels;
		ImageReader* reader = Get( fileName );
		width = reader->width() , height = reader->height() , channels = reader->channels();
		if( channels!=1 && channels!=3 ) ERROR_OUT( "Requres one- or three-channel input" );
		unsigned char* pixels = new unsigned char[ width*height*3 ];
		unsigned char* pixelRow = new unsigned char[ width*channels];
		for( unsigned int j=0 ; j<height ; j++ )
		{
			reader->nextRow( pixelRow );
			if     ( channels==3 ) memcpy( pixels+j*width*3 , pixelRow , sizeof(unsigned char)*width*3 );
			else if( channels==1 ) for( unsigned int i=0 ; i<width ; i++ ) for( unsigned int c=0 ; c<3 ; c++ ) pixels[j*width*3+i*3+c] = pixelRow[i];
		}
		delete[] pixelRow;
		delete reader;
		return pixels;
	}

	static bool ValidExtension( const char *ext );
	static ImageReader* Get( const char* fileName );
	static void GetInfo( const char* fileName , unsigned int& width , unsigned int& height , unsigned int& channels );
	virtual ~ImageReader( void ){ }
	unsigned int width( void ) const { return _width; }
	unsigned int height( void ) const { return _height; }
	unsigned int channels( void ) const { return _channels; }
protected:
	unsigned int _width , _height , _channels;
};
struct ImageWriterParams
{
	static const char* DefaultTileExtension;
	unsigned int quality;
#ifdef SUPPORT_TILES
	const char* tileExtension;
	unsigned int tileWidth , tileHeight;
	ImageWriterParams* tileParams;
	ImageWriterParams( void ) : quality( 100 ) , tileExtension( DefaultTileExtension ) , tileWidth( 4096 ) , tileHeight( 4096 ) , tileParams( NULL ){};
#else // !SUPPORT_TILES
	ImageWriterParams( void ) : quality( 100 ){};
#endif // SUPPORT_TILES
};
const char* ImageWriterParams::DefaultTileExtension = "jpg";
struct ImageWriter
{
	virtual unsigned int nextRow( const unsigned char* row ) = 0;
	virtual unsigned int nextRows( const unsigned char* rows , unsigned int rowNum ){ unsigned int row ; for( unsigned int r=0 ; r<rowNum ; r++ ) row = nextRow( rows + _width * _channels * r ) ; return row; }
	static void Write( const char* fileName , const unsigned char* pixels , unsigned int width , unsigned int height , unsigned int channels , ImageWriterParams params=ImageWriterParams() )
	{
		ImageWriter* writer = Get( fileName , width , height , channels , params );
		for( unsigned int j=0 ; j<height ; j++ ) writer->nextRow( pixels + j*width*channels );
		delete writer;
	}

	static bool ValidExtension( const char *ext );
	static ImageWriter* Get( const char* fileName , unsigned int width , unsigned int height , unsigned int channels , ImageWriterParams params=ImageWriterParams() );
	virtual ~ImageWriter( void ){ }
	unsigned int width( void ) const { return _width; }
	unsigned int height( void ) const { return _height; }
	unsigned int channels( void ) const { return _channels; }
protected:
	unsigned int _width , _height , _channels;
};

#ifdef SUPPORT_TILES
struct TiledImageReader : public ImageReader
{
	unsigned int nextRow( unsigned char* row );
	TiledImageReader( const char* fileName , unsigned int& width , unsigned int& height , unsigned int& channels );
	~TiledImageReader( void );
	static bool GetInfo( const char* fileName , unsigned int& width , unsigned int& height , unsigned int& channels );
protected:
	ImageReader** _tileReaders;
	char** _tileNames;
	unsigned int _tileRows , _tileColumns , _currentPixelRow , _currentTileRow , *_tileWidths , *_tileHeights;
};
struct TiledImageWriter : public ImageWriter
{
	unsigned int nextRow( const unsigned char* row );
	TiledImageWriter( const char* fileName , unsigned int width , unsigned int height , unsigned int channels , ImageWriterParams params );
	~TiledImageWriter( void );
protected:
	ImageWriter** _tileWriters;
	char** _tileNames;
	unsigned int _tileWidth , _tileHeight , _tileRows , _tileColumns , _currentPixelRow;
	ImageWriterParams _params;
};
#endif // SUPPORT_TILES


// [WARNING] Need to include "png.h" before "jpeg.h" so that "setjmp.h" is not already included (?)
#include "PNG.h"
#include "JPEG.h"

struct FileNameParser
{
#if defined( _WIN32 ) || defined( _WIN64 )
	static const char Separator = (char)'\\';
#else // !_WIN
	static const char Separator = (char)'/';
#endif // _WIN
	static inline char* Extension  ( const char* fileName ){ return __Split( fileName , '.' , false , false ); }
	static inline char* Header     ( const char* fileName ){ return __Split( fileName , '.' , true , false ); }
	static inline char* Local      ( const char* fileName ){ return __Split( fileName , Separator , false , false ); }
	static inline char* Dir        ( const char* fileName ){ return __Split( fileName , Separator , true , false ); }
	static inline char* LocalHeader( const char* fileName )
	{
		char* localFileName = Local( fileName );
		if( !localFileName ) ERROR_OUT( "Couldn't get local file name: " , fileName );
		char* localFileHeader = Header( localFileName );
		delete[] localFileName;
		return localFileHeader;
	}

protected:
	static inline char* __Split( const char* fileName , char splitChar , bool front , bool first )
	{
		int position;
		char* out;
		if( first ){ for( position=0 ; position<strlen(fileName) ; position++ ) if( fileName[position]==splitChar ) break; }
		else       { for( position=(int)strlen(fileName)-1 ; position>=0 ; position-- ) if( fileName[position]==splitChar ) break; }

		if( front )
		{
			if( position==-1 ) out = NULL;
			else
			{
				out = new char[ strlen(fileName)+1 ];
				strcpy( out , fileName );
				out[ position ] = 0;
			}
		}
		else
		{
			if( position==strlen(fileName) ) out = NULL;
			else
			{
				out = new char[ strlen(fileName)-position ];
				strcpy( out , fileName+position+1 );
			}
		}
		return out;
	}
};

inline bool ImageReader::ValidExtension( const char *ext )
{
#ifdef WIN32
	if     ( !_stricmp( ext , "jpeg" ) || !_stricmp( ext , "jpg" ) ) return true;
	else if( !_stricmp( ext , "png" )                              ) return true;
	else if( !_stricmp( ext , "iGrid" )                            ) return true;
#else // !WIN32
	if( !strcasecmp( ext , "jpeg" ) || !strcasecmp( ext , "jpg" ) ) return true;
	else if( !strcasecmp( ext , "png" )                           ) return true;
	else if( !strcasecmp( ext , "iGrid" )                         ) return true;
#endif // WIN32
	return false;
}

inline ImageReader* ImageReader::Get( const char* fileName )
{
	unsigned int width , height , channels;
	ImageReader* reader = NULL;
	char* ext = FileNameParser::Extension( fileName );
#ifdef WIN32
	if     ( !_stricmp( ext , "jpeg" ) || !_stricmp( ext , "jpg" ) ) reader = new       JPEGReader( fileName , width , height , channels );
	else if( !_stricmp( ext , "png" )                              ) reader = new        PNGReader( fileName , width , height , channels );
	else if( !_stricmp( ext , "iGrid" )                            ) reader = new TiledImageReader( fileName , width , height , channels );
#else // !WIN32
	if( !strcasecmp( ext , "jpeg" ) || !strcasecmp( ext , "jpg" ) ) reader = new       JPEGReader( fileName , width , height , channels );
	else if( !strcasecmp( ext , "png" )                           ) reader = new        PNGReader( fileName , width , height , channels );
	else if( !strcasecmp( ext , "iGrid" )                         ) reader = new TiledImageReader( fileName , width , height , channels );
#endif // WIN32
	else
	{
		delete[] ext;
		THROW( "failed to get image reader for: " , fileName );
	}
	reader->_width = width;
	reader->_height = height;
	reader->_channels = channels;

	delete[] ext;
	return reader;
}
inline void ImageReader::GetInfo( const char* fileName , unsigned int& width , unsigned int& height , unsigned int& channels )
{
	char* ext = FileNameParser::Extension( fileName );
#ifdef WIN32
	if( !_stricmp( ext , "jpeg" ) || !_stricmp( ext , "jpg" ) ) JPEGReader::GetInfo( fileName , width , height , channels );
	else if( !_stricmp( ext , "png" ) )                          PNGReader::GetInfo( fileName , width , height , channels );
	else if( !_stricmp( ext , "iGrid" ) )                 TiledImageReader::GetInfo( fileName , width , height , channels );
#else // !WIN32
	if( !strcasecmp( ext , "jpeg" ) || !strcasecmp( ext , "jpg" ) ) JPEGReader::GetInfo( fileName , width , height , channels );
	else if( !strcasecmp( ext , "png" ) )                            PNGReader::GetInfo( fileName , width , height , channels );
	else if( !strcasecmp( ext , "iGrid" ) )                   TiledImageReader::GetInfo( fileName , width , height , channels );
#endif // WIN32
	delete[] ext;
}

inline bool ImageWriter::ValidExtension( const char *ext )
{
#ifdef WIN32
	if( !_stricmp( ext , "jpeg" ) || !_stricmp( ext , "jpg" ) ) return true;
	else if( !_stricmp( ext , "png" ) )                         return true;
#ifdef SUPPORT_TILES
	else if( !_stricmp( ext , "iGrid" ) )                       return true;
#endif // SUPPORT_TILES
#else // !WIN32
	if( !strcasecmp( ext , "jpeg" ) || !strcasecmp( ext , "jpg" ) ) return true;
	else if( !strcasecmp( ext , "png" ) )                           return true;
#ifdef SUPPORT_TILES
	else if( !strcasecmp( ext , "iGrid" ) )                         return true;
#endif // SUPPORT_TILES
#endif // WIN32
	return false;
}

inline ImageWriter* ImageWriter::Get( const char* fileName , unsigned int width , unsigned int height , unsigned int channels , ImageWriterParams params )
{
	ImageWriter* writer = NULL;
	char* ext = FileNameParser::Extension( fileName );
#ifdef WIN32
	if( !_stricmp( ext , "jpeg" ) || !_stricmp( ext , "jpg" ) ) writer = new JPEGWriter( fileName , width , height , channels , params.quality );
	else if( !_stricmp( ext , "png" ) ) writer = new PNGWriter( fileName , width , height , channels , params.quality );
#ifdef SUPPORT_TILES
	else if( !_stricmp( ext , "iGrid" ) ) writer = new TiledImageWriter( fileName , width , height , channels , params );
#endif // SUPPORT_TILES
#else // !WIN32
	if( !strcasecmp( ext , "jpeg" ) || !strcasecmp( ext , "jpg" ) ) writer = new JPEGWriter( fileName , width , height , channels , params.quality );
	else if( !strcasecmp( ext , "png" ) ) writer = new PNGWriter( fileName , width , height , channels , params.quality );
#ifdef SUPPORT_TILES
	else if( !strcasecmp( ext , "iGrid" ) ) writer = new TiledImageWriter( fileName , width , height , channels , params );
#endif // SUPPORT_TILES
#endif // WIN32
	else
	{
		delete[] ext;
		THROW( "failed to get image writer for: " , fileName );
	}
	writer->_width = width;
	writer->_height = height;
	writer->_channels = channels;

	delete[] ext;
	return writer;
}

#ifdef SUPPORT_TILES
bool TiledImageReader::GetInfo( const char* fileName , unsigned int& width , unsigned int& height , unsigned int& channels )
{
	char* fileDir = FileNameParser::Dir( fileName );
	unsigned int *_tileHeights , *_tileWidths;
	unsigned int _tileRows , _tileColumns , _channels;
	FILE* fp = fopen( fileName , "r" );
	if( !fp ){ WARN( "Couldn't open file for reading: " , fileName ) ; return false; }
	{
		char line[1024];
		if( !fgets( line , 1024 , fp ) ) ERROR_OUT( "Failed to read column line from: " , fileName );
		line[strlen(line)-1] = 0;
		if( sscanf( line , "Columns: %d" , &_tileColumns )!=1 ) ERROR_OUT( "Failed to read column count from: " , fileName , " (" , line , ")" );
		if( !fgets( line , 1024 , fp ) ) ERROR_OUT( "Failed to read row line from: " , fileName );
		line[strlen(line)-1] = 0;
		if( sscanf( line , "Rows: %d" , &_tileRows )!=1 ) ERROR_OUT( "Failed to read row count from: " , fileName , " (" , line , ")" );
		_tileHeights = new unsigned int[ _tileRows+1 ];
		_tileWidths  = new unsigned int[ _tileColumns+1 ];

		char tileName[1024];
		for( unsigned int r=0 ; r<_tileRows ; r++ ) for( unsigned int c=0 ; c<_tileColumns ; c++ )
		{
			if( !fgets( line , 1024 , fp ) ) ERROR_OUT( "Failed to read tile name from: " , fileName );
			line[strlen(line)-1] = 0;
			if( fileDir ) sprintf( tileName , "%s%c%s" , fileDir , FileNameParser::Separator , line );
			else          sprintf( tileName , "%s" , line );

			unsigned int _w , _h , _c;
			ImageReader::GetInfo( tileName , _w , _h , _c );
			if( !r && !c ) _channels = _c;
			else if( _channels!=_c ) ERROR_OUT( "Number of color channels don't match: " , _channels , " != " , _c );
			if( !r ) _tileWidths[c+1] = _w;
			else if( _tileWidths[c+1]!=_w ) ERROR_OUT( "Images in the same column must have the same width: " , _tileWidths[c+1] , " != " , _w );
			if( !c ) _tileHeights[r+1] = _h;
			else if( _tileHeights[r+1]!=_h ) ERROR_OUT( "Images in the same row must have the same heights: " , _tileHeights[r+1] ," != " , _h );
		}
	}
	fclose( fp );
	if( fileDir ) delete[] fileDir;
	_tileWidths[0] = _tileHeights[0] = 0;
	for( unsigned int c=0 ; c<_tileColumns ; c++ ) _tileWidths[c+1] += _tileWidths[c];
	for( unsigned int r=0 ; r<_tileRows ; r++ ) _tileHeights[r+1] += _tileHeights[r];
	width = _tileWidths[_tileColumns] , height = _tileHeights[_tileRows] , channels = _channels;
	return true;
}

TiledImageReader::TiledImageReader( const char* fileName , unsigned int& width , unsigned int& height , unsigned int& channels )
{
	char* fileDir = FileNameParser::Dir( fileName );
	FILE* fp = fopen( fileName , "r" );
	if( !fp ) ERROR_OUT( "Couldn't open file for reading: " , fileName );
	{
		char line[1024];
		if( !fgets( line , 1024 , fp ) ) ERROR_OUT( "Failed read column line from: " , fileName );
		line[strlen(line)-1] = 0;
		if( sscanf( line , "Columns: %d" , &_tileColumns )!=1 ) ERROR_OUT( "Failed to read column count from: " , fileName , " (" , line , ")" );
		if( !fgets( line , 1024 , fp ) ) ERROR_OUT( "Failed read row line from: " , fileName );
		line[strlen(line)-1] = 0;
		if( sscanf( line , "Rows: %d" , &_tileRows )!=1 ) ERROR_OUT( "Failed to read row count from: " , fileName , " (" , line , ")" );

		_tileReaders = new ImageReader*[ _tileColumns ];
		_tileHeights = new unsigned int[ _tileRows+1 ];
		_tileWidths  = new unsigned int[ _tileColumns+1 ];

		_tileNames = new char*[ _tileColumns * _tileRows ];
		char tileName[1024];
		for( unsigned int r=0 ; r<_tileRows ; r++ ) for( unsigned int c=0 ; c<_tileColumns ; c++ )
		{
			if( !fgets( line , 1024 , fp ) ) ERROR_OUT( "Failed to read tile name from: " , fileName );
			line[strlen(line)-1] = 0;
			if( fileDir ) sprintf( tileName , "%s%c%s" , fileDir , FileNameParser::Separator , line );
			else          sprintf( tileName , "%s" , line );
			_tileNames[r*_tileColumns+c] = new char[ strlen(tileName)+1 ];
			strcpy( _tileNames[r*_tileColumns+c] , tileName );
		}
	}
	fclose( fp );
	if( fileDir ) delete[] fileDir;
	for( unsigned int r=0 ; r<_tileRows ; r++ ) for( unsigned int c=0 ; c<_tileColumns ; c++ )
	{
		unsigned int _w , _h , _c;
		ImageReader::GetInfo( _tileNames[r*_tileColumns+c] , _w , _h , _c );
		if( !r && !c ) _channels = _c;
		else if( _channels!=_c ) ERROR_OUT( "Number of color channels don't match: " , _channels , " != " , _c );
		if( !r ) _tileWidths[c+1] = _w;
		else if( _tileWidths[c+1]!=_w ) ERROR_OUT( "Images in the same column must have the same width: " , _tileWidths[c+1] , " != " , _w );
		if( !c ) _tileHeights[r+1] = _h;
		else if( _tileHeights[r+1]!=_h ) ERROR_OUT( "Images in the same row must have the same heights: " , _tileHeights[r+1] , " != " , _h );
	}
	_tileWidths[0] = _tileHeights[0] = 0;
	for( unsigned int c=0 ; c<_tileColumns ; c++ ) _tileWidths[c+1] += _tileWidths[c];
	for( unsigned int r=0 ; r<_tileRows ; r++ ) _tileHeights[r+1] += _tileHeights[r];
	width = _width = _tileWidths[_tileColumns] , height = _height = _tileHeights[_tileRows] , channels = _channels;
	_currentPixelRow = _currentTileRow = 0;
}
TiledImageReader::~TiledImageReader( void )
{
	delete[] _tileReaders;
	for( unsigned int i=0 ; i<_tileColumns*_tileRows ; i++ ) delete[] _tileNames[i];
	delete[] _tileNames;
	delete[] _tileWidths;
	delete[] _tileHeights;
}
unsigned TiledImageReader::nextRow( unsigned char* row )
{
	// If it's the first row, set up the readers
	if( _currentPixelRow==_tileHeights[ _currentTileRow ] ) for( unsigned int c=0 ; c<_tileColumns ; c++ ) _tileReaders[c] = ImageReader::Get( _tileNames[ _currentTileRow * _tileColumns + c ] );

	// Read the row fragments
	for( unsigned int c=0 ; c<_tileColumns ; c++ ) _tileReaders[c]->nextRow( row + c * _tileWidths[c] * _channels );

	// If it's the last row of the tile, free up the readers
	if( _currentPixelRow==_tileHeights[_currentTileRow+1]-1 )
	{
		for( unsigned int c=0 ; c<_tileColumns ; c++ ) delete _tileReaders[c];
		_currentTileRow++;
	}

	return _currentPixelRow++;
}

TiledImageWriter::TiledImageWriter( const char* fileName , unsigned int width , unsigned int height , unsigned int channels , ImageWriterParams params )
{
	_width = width , _height = height , _channels = channels , _tileWidth = params.tileWidth , _tileHeight = params.tileHeight;
	_tileColumns = ( _width + ( _tileWidth-1 ) ) / _tileWidth , _tileRows = ( _height + ( _tileHeight-1 ) ) / _tileHeight;
	_tileWriters = new ImageWriter*[ _tileColumns ];
	_tileNames = new char*[ _tileColumns * _tileRows ];
	if( params.tileParams ) _params = *params.tileParams;

	char tileName[1024];
	char* tileHeader = FileNameParser::Header( fileName );
	for( unsigned int r=0 ; r<_tileRows ; r++ ) for( unsigned int c=0 ; c<_tileColumns ; c++ )
	{
		sprintf( tileName , "%s.%d.%d.%s" , tileHeader , c , r , params.tileExtension );
		_tileNames[r*_tileColumns+c] = new char[ strlen(tileName)+1 ];
		strcpy( _tileNames[r*_tileColumns+c] , tileName );
	}
	delete[] tileHeader;
	FILE* fp = fopen( fileName , "w" );
	if( !fp ) ERROR_OUT( "Failed to open file for writing: " , fileName );
	fprintf( fp , "Columns: %d\n" , _tileColumns );
	fprintf( fp , "Rows: %d\n" , _tileRows );
	for( unsigned int i=0 ; i<_tileRows*_tileColumns ; i++ )
	{
		char* localTileName = FileNameParser::Local( _tileNames[i] );
		fprintf( fp , "%s\n" , localTileName );
		delete[] localTileName;
	}
	fclose( fp );
	_currentPixelRow = 0;
}
TiledImageWriter::~TiledImageWriter( void )
{
	delete[] _tileWriters;
	for( unsigned int i=0 ; i<_tileColumns*_tileRows ; i++ ) delete[] _tileNames[i];
	delete[] _tileNames;
}
unsigned int TiledImageWriter::nextRow( const unsigned char* row )
{
	unsigned int r = _currentPixelRow / _tileHeight;
	if( ( _currentPixelRow % _tileHeight )==0 )
	{
		for( unsigned int c=0 ; c<_tileColumns ; c++ )
			_tileWriters[c] = ImageWriter::Get( _tileNames[ r * _tileColumns + c ] , std::min< unsigned int >( _tileWidth , _width - _tileWidth*c ) , std::min< unsigned int >( _tileHeight , _height - _tileHeight*r ) , _channels , _params );
	}
	for( int c=0 ; c<(int)_tileColumns ; c++ ) _tileWriters[c]->nextRow( row + c * _tileWidth * _channels );
	if( ( _currentPixelRow % _tileHeight )==( _tileHeight-1 ) || _currentPixelRow==(_height-1) ) for( unsigned int c=0 ; c<_tileColumns ; c++ ) delete _tileWriters[c];

	return _currentPixelRow++;
}
#endif // SUPPORT_TILES

#endif // IMAGE_INCLUDED
