// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.


#include <iostream>

#include "nel/misc/file.h"
#include "nel/misc/common.h"
#include "nel/misc/bitmap.h"
#include "nel/misc/path.h"
#include "nel/misc/debug.h"
#include "nel/misc/cmd_args.h"

#include <math.h>

#include "../s3tc_compressor_lib/s3tc_compressor.h"


using namespace NLMISC;
using namespace std;

#ifdef DEBUG_NEW
#define new DEBUG_NEW
#endif

#define	TGA8	8
#define	TGA16	16
#define	PNG8	108
#define	PNG16	116
#define NOT_DEFINED 0xff


bool sameType(const std::string &sFileNameDest, uint8 algo);
bool dataCheck(const std::string &sFileNameSrc, const std::string &FileNameDest, uint8 algo);
std::string getOutputFileName(const std::string &inputFileName);




uint8 getType(const std::string &sFileNameDest)
{
	uint32 dds;
	FILE *f = nlfopen(sFileNameDest, "rb");
	if(f==NULL)
	{
		return NOT_DEFINED;
	}
	CS3TCCompressor::DDS_HEADER h;

	if (fread(&dds,1,4,f) != 4)
	{
		fclose(f);
		return NOT_DEFINED;
	}

#ifdef NL_BIG_ENDIAN
	NLMISC_BSWAP32(dds);
#endif

	if (fread(&h,sizeof(CS3TCCompressor::DDS_HEADER),1,f) != 1)
	{
		fclose(f);
		return NOT_DEFINED;
	}

	if(fclose(f))
	{
		cerr<<sFileNameDest<< "is not closed"<<endl;
	}

	if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '1')
		&& h.ddpf.dwRGBBitCount==0)
	{
		return DXT1;
	}

	if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '1')
		&& h.ddpf.dwRGBBitCount>0)
	{
		return DXT1A;
	}

	if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '3'))
	{
		return DXT3;
	}

	if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '5'))
	{
		return DXT5;
	}

	return NOT_DEFINED;
}

bool sameType(const std::string &sFileNameDest, uint8 &algo, bool wantMipMap)
{
	uint32 dds;
	FILE *f = nlfopen(sFileNameDest, "rb");
	if(f==NULL)
	{
		return false;
	}

	CS3TCCompressor::DDS_HEADER h;

	if (fread(&dds,1,4,f) != 4)
	{
		fclose(f);
		return false;
	}

	if (fread(&h,sizeof(::DDS_HEADER),1,f) != 1)
	{
		fclose(f);
		return false;
	}

	if(fclose(f))
	{
		cerr<<sFileNameDest<< "is not closed"<<endl;
	}

	bool	algoOk= false;
	switch(algo)
	{
		case DXT1:
			if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '1')
				&& h.ddpf.dwRGBBitCount==0)
				algoOk=true;
			break;

		case DXT1A:
			if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '1')
				&& h.ddpf.dwRGBBitCount>0)
				algoOk=true;
			break;

		case DXT3:
			if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '3'))
				algoOk=true;
			break;

		case DXT5:
			if(h.ddpf.dwFourCC==MAKEFOURCC('D','X', 'T', '5'))
				algoOk=true;
			break;
	}
	if(!algoOk)
		return false;

	// Test Mipmap.
	bool	fileHasMipMap= (h.dwFlags&DDSD_MIPMAPCOUNT) && (h.dwMipMapCount>1);
	if(fileHasMipMap==wantMipMap)
		return true;

	return false;
}



bool dataCheck(const std::string &sFileNameSrc, const std::string &sFileNameDest, uint8& algo, bool wantMipMap)
{
	if (!CFile::fileExists(sFileNameSrc))
	{
		cerr << "Can't open file " << sFileNameSrc << endl;
		return false;
	}

	if (!CFile::fileExists(sFileNameDest))
	{
		return false; // destination file doesn't exist yet
	}

	uint32 lastWriteTime1 = CFile::getFileModificationDate(sFileNameSrc);
	uint32 lastWriteTime2 = CFile::getFileModificationDate(sFileNameDest);

	if(lastWriteTime1 > lastWriteTime2)
	{
		return false;
	}
	if (lastWriteTime1 < lastWriteTime2)
	{
		if(!sameType(sFileNameDest, algo, wantMipMap))
		{
			return false; // file exists but a new compression type is required
		}
	}
	return true;
}

std::string getOutputFileName(const std::string &inputFileName)
{
	std::string::size_type pos = inputFileName.rfind(".");
	if(pos == std::string::npos)
	{
		// name whithout extension
		return inputFileName + ".dds";
	}
	else
	{
		return inputFileName.substr(0,pos) + ".dds";
	}
}

// ***************************************************************************
void dividSize (CBitmap &bitmap)
{
	// Must be RGBA
	nlassert (bitmap.getPixelFormat () == CBitmap::RGBA);

	// Copy the bitmap
	CBitmap temp = bitmap;

	// Resize the destination
	const uint width = temp.getWidth ();
	const uint height = temp.getHeight ();
	const uint newWidth = temp.getWidth ()/2;
	const uint newHeight = temp.getHeight ()/2;
	bitmap.resize (newWidth, newHeight, CBitmap::RGBA);

	// Pointers
	uint8 *pixelSrc = &(temp.getPixels ()[0]);
	uint8 *pixelDest = &(bitmap.getPixels ()[0]);

	// Resample
	uint x, y;
	for (y=0; y<newHeight; y++)
	for (x=0; x<newWidth; x++)
	{
		const uint offsetSrc = ((y*2)*width+x*2)*4;
		const uint offsetDest = (y*newWidth+x)*4;
		uint i;
		for (i=0; i<4; i++)
		{
			pixelDest[offsetDest+i] = ((uint)pixelSrc[offsetSrc+i] + (uint)pixelSrc[offsetSrc+4+i] +
				(uint)pixelSrc[offsetSrc+4*width+i] + (uint)pixelSrc[offsetSrc+4*width+4+i])>>2;
		}
	}
}

const int bayerDiv8R[4][4] = {
	{ 7, 3, 6, 2 },
	{ 1, 5, 0, 4 },
	{ 6, 2, 7, 3 },
	{ 0, 4, 1, 5 }
};

const int bayerDiv8G[4][4] = {
	{ 0, 4, 1, 5 },
	{ 6, 2, 7, 3 },
	{ 1, 5, 0, 4 },
	{ 7, 3, 6, 2 }
};

const int bayerDiv8B[4][4] = {
	{ 5, 1, 4, 0 },
	{ 3, 7, 2, 6 },
	{ 4, 0, 5, 1 },
	{ 2, 6, 3, 7 }
};

// ***************************************************************************
int main(int argc, char **argv)
{
	CApplicationContext applicationContext;

	// Parse Command Line.
	//====================
	NLMISC::CCmdArgs args;

	args.setDescription(
		"Convert TGA or PNG image file to DDS compressed file using DXTC compression (DXTC1, DXTC1 with alpha, DXTC3, or DXTC5).\n"
		"  The program looks for possible user color files and load them automatically, a user color file must have the same name that the original tga file, plus the extension \"_usercolor\"\n"
		"Eg.: pic.tga, the associated user color file must be: pic_usercolor.tga\n"
		);
	args.addArg("o", "output", "output.dds", "Output DDS filename or directory");
	args.addArg("a", "algo", "algo", "Conversion algorithm to use\n"
		"  1      for DXTC1 (no alpha)\n"
		"  1A     for DXTC1 with alpha\n"
		"  3      for DXTC3\n"
		"  5      for DXTC5\n"
		"  tga16  for 16 bits TGA\n"
		"  tga8   for 8  bits TGA\n"
		"  png16  for 16 bits PNG\n"
		"  png8   for 8  bits PNG\n"
		"\n"
		"  default : DXTC1 if 24 bits, DXTC5 if 32 bits."
		);
	args.addArg("g", "grayscale", "", "Don't load grayscape images as alpha but as grayscale");
	args.addArg("m", "mipmap", "", "Create MipMap");
	args.addArg("r", "reduce", "FACTOR", "Reduce the bitmap size before compressing\n  FACTOR is 0, 1, 2, 3, 4, 5, 6, 7 or 8");
	args.addAdditionalArg("input", "PNG or TGA files to convert", false);

	if (!args.parse(argc, argv)) return 1;

	string OptOutputFileName;
	uint8 OptAlgo = NOT_DEFINED;
	bool OptMipMap = false;
	bool OptGrayscale = false;
	uint Reduce = 0;

	if (args.haveArg("o"))
		OptOutputFileName = args.getArg("o").front();

	if (args.haveArg("m"))
		OptMipMap = true;

	if (args.haveArg("g"))
		OptGrayscale = true;

	if (args.haveArg("a"))
	{
		std::string strAlgo = args.getArg("a").front();

		if (strAlgo == "1")					OptAlgo = DXT1;
		else if (toLower(strAlgo) == "1a") 	OptAlgo = DXT1A;
		else if (strAlgo == "3")			OptAlgo = DXT3;
		else if (strAlgo == "5") 			OptAlgo = DXT5;
		else if (strAlgo == "tga8")			OptAlgo = TGA8;
		else if (strAlgo == "tga16")		OptAlgo = TGA16;
		else if (strAlgo == "png8")			OptAlgo = PNG8;
		else if (strAlgo == "png16")		OptAlgo = PNG16;
		else
		{
			cerr << "Unknown algorithm: " << strAlgo << endl;
			return 1;
		}
	}

	if (args.haveArg("r"))
	{
		std::string strReduce = args.getArg("r").front();

		// Reduce size of the bitmap
		if (fromString(strReduce, Reduce))
		{
			if (Reduce > 8) Reduce = 8;
		}
	}

	std::vector<std::string> inputFileNames = args.getAdditionalArg("input");

	for(uint i = 0; i < inputFileNames.size(); ++i)
	{
		uint8 algo;

		// Reading TGA or PNG and converting to RGBA
		//====================================
		CBitmap picTga;
		CBitmap picTga2;
		CBitmap picSrc;

		std::string inputFileName = inputFileNames[i];

		if(inputFileName.find("_usercolor")<inputFileName.length())
		{
			return 0;
		}

		NLMISC::CIFile input;
		if(!input.open(inputFileName))
		{
			cerr<<"Can't open input file " << inputFileName << endl;
			return 1;
		}

		// allow to load an image as grayscale instead of alpha
		if (OptGrayscale) picTga.loadGrayscaleAsAlpha(false);

		uint8 imageDepth = picTga.load(input);
		if(imageDepth==0)
		{
			cerr<<"Can't load file: "<<inputFileName<<endl;
			return 1;
		}
		if(imageDepth!=16 && imageDepth!=24 && imageDepth!=32 && imageDepth!=8)
		{
			cerr<<"Image not supported: "<<imageDepth<<endl;
			return 1;
		}
		input.close();
		uint32 height = picTga.getHeight();
		uint32 width= picTga.getWidth();
		picTga.convertToType (CBitmap::RGBA);


		// Output file name and algo.
		//===========================
		std::string outputFileName;

		if (!OptOutputFileName.empty())
		{
			// if OptOutputFileName is a directory, append the original filename
			if (CFile::isDirectory(OptOutputFileName))
			{
				outputFileName = CPath::standardizePath(OptOutputFileName) + CFile::getFilename(getOutputFileName(inputFileName));
			}
			else
			{
				outputFileName = OptOutputFileName;

				if (inputFileNames.size() > 1)
				{
					cerr<<"WARNING! Several files to convert to the same output filename! Use an output directory instead."<<endl;
					return 1;
				}
			}
		}
		else
		{
			outputFileName = getOutputFileName(inputFileName);
		}

		// Check dest algo
		if (OptAlgo==NOT_DEFINED)
			OptAlgo = getType (outputFileName);

		// Choose Algo.
		if(OptAlgo!=NOT_DEFINED)
		{
			algo= OptAlgo;
		}
		else
		{
			// TODO: if alpha channel is 0, use DXTC1a instead DXTC1
			if(imageDepth==24)
				algo = DXT1;
			else
				algo = DXT5;
		}

		// Data check
		//===========
		if(dataCheck(inputFileName,outputFileName, OptAlgo, OptMipMap))
		{
			cout<<outputFileName<<" : a recent dds file already exists"<<endl;
			return 0;
		}


		// Vectors for RGBA data
		CObjectVector<uint8> RGBASrc = picTga.getPixels();
		CObjectVector<uint8> RGBASrc2;
		CObjectVector<uint8> RGBADest;
		RGBADest.resize(height*width*4);
		uint	dstRGBADestId= 0;

		// UserColor
		//===========
		/*
		// Checking if option "usercolor" has been used
		std::string userColorFileName;
		if(argc>4)
		{
			if(strcmp("-usercolor",argv[4])==0)
			{
				if(argc!=6)
				{
					writeInstructions();
					return;
				}
				userColorFileName = argv[5];
			}
			else
			{
				writeInstructions();
				return;
			}
		}
		*/
		// Checking if associate usercolor file  exists
		std::string userColorFileName;
		std::string::size_type pos = inputFileName.rfind(".");
		if (pos == std::string::npos)
		{
			// name without extension
			userColorFileName = inputFileName + "_usercolor";
		}
		else
		{
			// append input filename extension
			userColorFileName = inputFileName.substr(0,pos) + "_usercolor" + inputFileName.substr(pos);
		}

		// Reading second Tga for user color, don't complain if _usercolor is missing
		NLMISC::CIFile input2;
		if (CPath::exists(userColorFileName) && input2.open(userColorFileName))
		{
			picTga2.load(input2);
			uint32 height2 = picTga2.getHeight();
			uint32 width2 = picTga2.getWidth();
			nlassert(width2==width);
			nlassert(height2==height);
			picTga2.convertToType (CBitmap::RGBA);

			RGBASrc2 = picTga2.getPixels();

			NLMISC::CRGBA *pRGBASrc = (NLMISC::CRGBA*)&RGBASrc[0];
			NLMISC::CRGBA *pRGBASrc2 = (NLMISC::CRGBA*)&RGBASrc2[0];

			for(uint32 i = 0; i<width*height; i++)
			{
				// If no UserColor, must take same RGB, and keep same Alpha from src1 !!! So texture can have both alpha
				// userColor and other alpha usage.
				if(pRGBASrc2[i].A==255)
				{
					RGBADest[dstRGBADestId++]= pRGBASrc[i].R;
					RGBADest[dstRGBADestId++]= pRGBASrc[i].G;
					RGBADest[dstRGBADestId++]= pRGBASrc[i].B;
					RGBADest[dstRGBADestId++]= pRGBASrc[i].A;
				}
				else
				{
					// Old code.
					/*uint8 F = (uint8) ((float)pRGBASrc[i].R*0.3 + (float)pRGBASrc[i].G*0.56 + (float)pRGBASrc[i].B*0.14);
					uint8 Frgb;
					if((F*pRGBASrc2[i].A/255)==255)
						Frgb = 0;
					else
						Frgb = (255-pRGBASrc2[i].A)/(255-F*pRGBASrc2[i].A/255);
					RGBADest[dstRGBADestId++]= Frgb*pRGBASrc[i].R/255;
					RGBADest[dstRGBADestId++]= Frgb*pRGBASrc[i].G/255;
					RGBADest[dstRGBADestId++]= Frgb*pRGBASrc[i].B/255;
					RGBADest[dstRGBADestId++]= F*pRGBASrc[i].A/255;*/

					// New code: use new restrictions from IDriver.
					float	Rt, Gt, Bt, At;
					float	Lt;
					float	Rtm, Gtm, Btm, Atm;

					// read 0-1 RGB pixel.
					Rt= (float)pRGBASrc[i].R/255;
					Gt= (float)pRGBASrc[i].G/255;
					Bt= (float)pRGBASrc[i].B/255;
					Lt= Rt*0.3f + Gt*0.56f + Bt*0.14f;

					// take Alpha from userColor src.
					At= (float)pRGBASrc2[i].A/255;
					Atm= 1-Lt*(1-At);

					// If normal case.
					if(Atm>0)
					{
						Rtm= Rt*At / Atm;
						Gtm= Gt*At / Atm;
						Btm= Bt*At / Atm;
					}
					// Else special case: At==0, and Lt==1.
					else
					{
						Rtm= Gtm= Btm= 0;
					}

					// copy to buffer.
					sint	r,g,b,a;
					r= (sint)floor(Rtm*255+0.5f);
					g= (sint)floor(Gtm*255+0.5f);
					b= (sint)floor(Btm*255+0.5f);
					a= (sint)floor(Atm*255+0.5f);
					clamp(r, 0,255);
					clamp(g, 0,255);
					clamp(b, 0,255);
					clamp(a, 0,255);
					RGBADest[dstRGBADestId++]= r;
					RGBADest[dstRGBADestId++]= g;
					RGBADest[dstRGBADestId++]= b;
					RGBADest[dstRGBADestId++]= a;
				}
			}
		}
		else
			RGBADest = RGBASrc;

		// Copy to the dest bitmap.
		picSrc.resize(width, height, CBitmap::RGBA);
		picSrc.getPixels(0)= RGBADest;

		// Resize the destination bitmap ?
		while (Reduce != 0)
		{
			dividSize (picSrc);
			Reduce--;
		}

		if (algo == TGA16)
		{
			// Apply bayer dither
			CObjectVector<uint8> &rgba = picSrc.getPixels(0);
			const uint32 w = picSrc.getWidth(0);
			uint32 x = 0;
			uint32 y = 0;
			for (uint32 i = 0; i < rgba.size(); i += 4)
			{
				NLMISC::CRGBA &c = reinterpret_cast<NLMISC::CRGBA &>(rgba[i]);
				c.R = (uint8)std::min(255, (int)c.R + bayerDiv8R[x % 4][y % 4]);
				c.G = (uint8)std::min(255, (int)c.G + bayerDiv8G[x % 4][y % 4]);
				c.B = (uint8)std::min(255, (int)c.B + bayerDiv8B[x % 4][y % 4]);
				++x;
				x %= w;
				if (x == 0)
					++y;
			}
		}

		// 8 or 16 bits TGA or PNG ?
		if ((algo == TGA16) || (algo == TGA8) || (algo == PNG16) || (algo == PNG8))
		{
			// Saving TGA or PNG file
			//=================
			NLMISC::COFile output;
			if(!output.open(outputFileName))
			{
				cerr<<"Can't open output file "<<outputFileName<<endl;
				return 1;
			}
			try
			{
				if (algo == TGA16)
				{
					picSrc.writeTGA (output, 16);
				}
				else if (algo == TGA8)
				{
					picSrc.convertToType(CBitmap::Luminance);
					picSrc.writeTGA (output, 8);
				}
				else if (algo == PNG16)
				{
					picSrc.writePNG (output, 16);
				}
				else if (algo == PNG8)
				{
					picSrc.convertToType(CBitmap::Luminance);
					picSrc.writePNG (output, 8);
				}
			}
			catch(const NLMISC::EWriteError &e)
			{
				cerr<<e.what()<<endl;
				return 1;
			}

			output.close();
		}
		else
		{
			// Compress
			//===========

			// log.
			std::string algostr;
			switch(algo)
			{
				case DXT1:
					algostr = "DXTC1";
					break;
				case DXT1A:
					algostr = "DXTC1A";
					break;
				case DXT3:
					algostr = "DXTC3";
					break;
				case DXT5:
					algostr = "DXTC5";
					break;
			}
			cout<<"compressing ("<<algostr<<") "<<inputFileName<<" to "<<outputFileName<<endl;


			// Saving compressed DDS file
			// =================
			NLMISC::COFile output;
			if(!output.open(outputFileName))
			{
				cerr<<"Can't open output file "<<outputFileName<<endl;
				return 1;
			}
			try
			{
				CS3TCCompressor		comp;
				comp.compress(picSrc, OptMipMap, algo, output);
			}
			catch(const NLMISC::EWriteError &e)
			{
				cerr<<e.what()<<endl;
				return 1;
			}

			output.close();
		}
	}

	return 0;
}