#include "Format.h"

#include <ctime>
#include <stdexcept>
#include <iostream>
#include <iterator>
#include <cstring>
#include <zlib.h>
#include <cstdio>

#include "graphics/Graphics.h"

ByteString format::UnixtimeToDate(time_t unixtime, ByteString dateFormat)
{
	struct tm * timeData;
	char buffer[128];

	timeData = localtime(&unixtime);

	strftime(buffer, 128, dateFormat.c_str(), timeData);
	return ByteString(buffer);
}

ByteString format::UnixtimeToDateMini(time_t unixtime)
{
	time_t currentTime = time(NULL);
	struct tm currentTimeData = *gmtime(&currentTime);
	struct tm timeData = *gmtime(&unixtime);

	if(currentTimeData.tm_year != timeData.tm_year)
	{
		return UnixtimeToDate(unixtime, "%d %b %Y");
	}
	else if(currentTimeData.tm_mon != timeData.tm_mon || currentTimeData.tm_mday != timeData.tm_mday)
	{
		return UnixtimeToDate(unixtime, "%d %B");
	}
	else
	{
		return UnixtimeToDate(unixtime, "%H:%M:%S");
	}
}

String format::CleanString(String dirtyString, bool ascii, bool color, bool newlines, bool numeric)
{
	for (size_t i = 0; i < dirtyString.size(); i++)
	{
		switch(dirtyString[i])
		{
		case '\b':
			if (color)
			{
				dirtyString.erase(i, 2);
				i--;
			}
			else
				i++;
			break;
		case '\x0E':
			if (color)
			{
				dirtyString.erase(i, 1);
				i--;
			}
			break;
		case '\x0F':
			if (color)
			{
				dirtyString.erase(i, 4);
				i--;
			}
			else
				i += 3;
			break;
		case '\r':
		case '\n':
			if (newlines)
				dirtyString[i] = ' ';
			break;
		default:
			if (numeric && (dirtyString[i] < '0' || dirtyString[i] > '9'))
			{
				dirtyString.erase(i, 1);
				i--;
			}
			// if less than ascii 20 or greater than ascii 126, delete
			else if (ascii && (dirtyString[i] < ' ' || dirtyString[i] > '~'))
			{
				dirtyString.erase(i, 1);
				i--;
			}
			break;
		}
	}
	return dirtyString;
}

std::vector<char> format::VideoBufferToPTI(const VideoBuffer & vidBuf)
{
	std::vector<char> data;
	int dataSize = 0;
	char * buffer = (char*)Graphics::ptif_pack(vidBuf.Buffer, vidBuf.Width, vidBuf.Height, &dataSize);

	if(buffer)
	{
		data.insert(data.end(), buffer, buffer+dataSize);
		free(buffer);
	}

	return data;
}

VideoBuffer * format::PTIToVideoBuffer(std::vector<char> & data)
{

	int newWidth, newHeight;
	pixel * buffer = Graphics::ptif_unpack(&data[0], data.size(), &newWidth, &newHeight);

	if(buffer)
	{
		VideoBuffer * vb = new VideoBuffer(buffer, newWidth, newHeight);
		free(buffer);
		return vb;
	}
	return NULL;
}

std::vector<char> format::VideoBufferToBMP(const VideoBuffer & vidBuf)
{
	std::vector<char> data;
	char buffer[54] = "BM";
	int padding = 3 - (vidBuf.Width * 3 + 3) % 4;
	unsigned int fileSize = (vidBuf.Width * 3 + padding) * vidBuf.Height + 54;
	*(int *)(buffer + 2) = fileSize;
	*(short int *)(buffer + 6) = 0; // reserved;
	*(short int *)(buffer + 8) = 0; // reserved;
	*(int *)(buffer + 10) = 0x36; // 54 bytes from start to data
	*(int *)(buffer + 14) = 0x28; // 40 bytes in info header
	*(int *)(buffer + 18) = vidBuf.Width;
	*(int *)(buffer + 22) = vidBuf.Height;
	*(short int *)(buffer + 26) = 1; // 1 plane
	*(short int *)(buffer + 28) = 24; // 24 bits per pixel
	*(int *)(buffer + 30) = 0; // no compression
	*(int *)(buffer + 34) = fileSize - 54; // bitmap size
	*(int *)(buffer + 38) = 0xB13; // 72dpi
	*(int *)(buffer + 42) = 0xB13; // 72dpi
	*(int *)(buffer + 46) = 0; // all colors used
	*(int *)(buffer + 50) = 0; // all colors important

	data.insert(data.end(), buffer, buffer+54);

	char *currentRow = (char *)malloc((vidBuf.Width * 3) + padding);
	for(int y = vidBuf.Height - 1; y >= 0; y--)
	{
		int rowPos = 0;
		for(int x = 0; x < vidBuf.Width; x++)
		{
			currentRow[rowPos++] = PIXB(vidBuf.Buffer[(y*vidBuf.Width)+x]);
			currentRow[rowPos++] = PIXG(vidBuf.Buffer[(y*vidBuf.Width)+x]);
			currentRow[rowPos++] = PIXR(vidBuf.Buffer[(y*vidBuf.Width)+x]);
		}
		data.insert(data.end(), currentRow, currentRow+(vidBuf.Width*3)+padding);
	}
	free(currentRow);

	return data;
}

std::vector<char> format::VideoBufferToPPM(const VideoBuffer & vidBuf)
{
	std::vector<char> data;
	char buffer[256];
	sprintf(buffer, "P6\n%d %d\n255\n", vidBuf.Width, vidBuf.Height);
	data.insert(data.end(), buffer, buffer+strlen(buffer));

	unsigned char * currentRow = new unsigned char[vidBuf.Width*3];
	for(int y = 0; y < vidBuf.Height; y++)
	{
		int rowPos = 0;
		for(int x = 0; x < vidBuf.Width; x++)
		{
			currentRow[rowPos++] = PIXR(vidBuf.Buffer[(y*vidBuf.Width)+x]);
			currentRow[rowPos++] = PIXG(vidBuf.Buffer[(y*vidBuf.Width)+x]);
			currentRow[rowPos++] = PIXB(vidBuf.Buffer[(y*vidBuf.Width)+x]);
		}
		data.insert(data.end(), currentRow, currentRow+(vidBuf.Width*3));
	}
	delete [] currentRow;

	return data;
}

struct PNGChunk
{
	int Length;
	char Name[4];
	char * Data;

	//char[4] CRC();

	PNGChunk(int length, ByteString name)
	{
		if (name.length()!=4)
			throw std::runtime_error("Invalid chunk name");
		std::copy(name.begin(), name.begin()+4, Name);
		Length = length;
		if (length)
		{
			Data = new char[length];
			std::fill(Data, Data+length, 0);
		}
		else
		{
			Data = NULL;
		}
	}
	unsigned long CRC()
	{
		if (!Data)
		{
			return format::CalculateCRC((unsigned char*)Name, 4);
		}
		else
		{
			unsigned char * temp = new unsigned char[4+Length];
			std::copy(Name, Name+4, temp);
			std::copy(Data, Data+Length, temp+4);
			unsigned long tempRet = format::CalculateCRC(temp, 4+Length);
			delete[] temp;
			return tempRet;
		}
	}
	~PNGChunk()
	{
		delete[] Data;
	}
};

std::vector<char> format::VideoBufferToPNG(const VideoBuffer & vidBuf)
{
	std::vector<PNGChunk*> chunks;

	//Begin IHDR (Image header) chunk (Image size and depth)
	PNGChunk *IHDRChunk = new PNGChunk(13, "IHDR");

	//Image Width
	IHDRChunk->Data[0] = (vidBuf.Width>>24)&0xFF;
	IHDRChunk->Data[1] = (vidBuf.Width>>16)&0xFF;
	IHDRChunk->Data[2] = (vidBuf.Width>>8)&0xFF;
	IHDRChunk->Data[3] = (vidBuf.Width)&0xFF;

	//Image Height
	IHDRChunk->Data[4] = (vidBuf.Height>>24)&0xFF;
	IHDRChunk->Data[5] = (vidBuf.Height>>16)&0xFF;
	IHDRChunk->Data[6] = (vidBuf.Height>>8)&0xFF;
	IHDRChunk->Data[7] = (vidBuf.Height)&0xFF;

	//Bit depth
	IHDRChunk->Data[8] = 8; //8bits per channel or 24bpp

	//Colour type
	IHDRChunk->Data[9] = 2; //RGB triple

	//Everything else is default
	chunks.push_back(IHDRChunk);

	//Begin image data, format is 8bit RGB (24bit pixel)
	int dataPos = 0;
	unsigned char * uncompressedData = new unsigned char[(vidBuf.Width*vidBuf.Height*3)+vidBuf.Height];

	//Byte ordering and filtering
	unsigned char * previousRow = new unsigned char[vidBuf.Width*3];
	std::fill(previousRow, previousRow+(vidBuf.Width*3), 0);
	unsigned char * currentRow = new unsigned char[vidBuf.Width*3];
	for(int y = 0; y < vidBuf.Height; y++)
	{
		int rowPos = 0;
		for(int x = 0; x < vidBuf.Width; x++)
		{
			currentRow[rowPos++] = PIXR(vidBuf.Buffer[(y*vidBuf.Width)+x]);
			currentRow[rowPos++] = PIXG(vidBuf.Buffer[(y*vidBuf.Width)+x]);
			currentRow[rowPos++] = PIXB(vidBuf.Buffer[(y*vidBuf.Width)+x]);
		}

		uncompressedData[dataPos++] = 2; //Up Sub(x) filter
		for(int b = 0; b < rowPos; b++)
		{
			int filteredByte = (currentRow[b]-previousRow[b])&0xFF;
			uncompressedData[dataPos++] = filteredByte;
		}

		unsigned char * tempRow = previousRow;
		previousRow = currentRow;
		currentRow = tempRow;
	}
	delete[] currentRow;
	delete[] previousRow;

	//Compression
	int compressedBufferSize = (vidBuf.Width*vidBuf.Height*3)*2;
	unsigned char * compressedData = new unsigned char[compressedBufferSize];

	int result;
    z_stream zipStream;
    zipStream.zalloc = Z_NULL;
    zipStream.zfree = Z_NULL;
    zipStream.opaque = Z_NULL;

    result = deflateInit2(&zipStream,
            9,              // level
            Z_DEFLATED,     // method
            10,             // windowBits
            1,              // memLevel
            Z_DEFAULT_STRATEGY // strategy
    );

    if (result != Z_OK) exit(result);

    zipStream.next_in = uncompressedData;
    zipStream.avail_in = dataPos;

    zipStream.next_out = compressedData;
    zipStream.avail_out = compressedBufferSize;


    result = deflate(&zipStream, Z_FINISH);
    if (result != Z_STREAM_END) exit(result);

    int compressedSize = compressedBufferSize-zipStream.avail_out;
	PNGChunk *IDATChunk = new PNGChunk(compressedSize, "IDAT");
	std::copy(compressedData, compressedData+compressedSize, IDATChunk->Data);
	chunks.push_back(IDATChunk);

	deflateEnd(&zipStream);

	delete[] compressedData;
	delete[] uncompressedData;

	PNGChunk *IENDChunk = new PNGChunk(0, "IEND");
	chunks.push_back(IENDChunk);

	//Write chunks to output buffer
	int finalDataSize = 8;
	for(std::vector<PNGChunk*>::iterator iter = chunks.begin(), end = chunks.end(); iter != end; ++iter)
	{
		PNGChunk * cChunk = *iter;
		finalDataSize += 4 + 4 + 4;
		finalDataSize += cChunk->Length;
	}
	unsigned char * finalData = new unsigned char[finalDataSize];
	int finalDataPos = 0;

	//PNG File header
	finalData[finalDataPos++] = 0x89;
	finalData[finalDataPos++] = 0x50;
	finalData[finalDataPos++] = 0x4E;
	finalData[finalDataPos++] = 0x47;
	finalData[finalDataPos++] = 0x0D;
	finalData[finalDataPos++] = 0x0A;
	finalData[finalDataPos++] = 0x1A;
	finalData[finalDataPos++] = 0x0A;

	for(std::vector<PNGChunk*>::iterator iter = chunks.begin(), end = chunks.end(); iter != end; ++iter)
	{
		PNGChunk * cChunk = *iter;

		//Chunk length
		finalData[finalDataPos++] = (cChunk->Length>>24)&0xFF;
		finalData[finalDataPos++] = (cChunk->Length>>16)&0xFF;
		finalData[finalDataPos++] = (cChunk->Length>>8)&0xFF;
		finalData[finalDataPos++] = (cChunk->Length)&0xFF;

		//Chunk name
		std::copy(cChunk->Name, cChunk->Name+4, finalData+finalDataPos);
		finalDataPos += 4;

		//Chunk data
		if(cChunk->Data)
		{
			std::copy(cChunk->Data, cChunk->Data+cChunk->Length, finalData+finalDataPos);
			finalDataPos += cChunk->Length;
		}

		//Chunk CRC
		unsigned long tempCRC = cChunk->CRC();
		finalData[finalDataPos++] = (tempCRC>>24)&0xFF;
		finalData[finalDataPos++] = (tempCRC>>16)&0xFF;
		finalData[finalDataPos++] = (tempCRC>>8)&0xFF;
		finalData[finalDataPos++] = (tempCRC)&0xFF;

		delete cChunk;
	}

	std::vector<char> outputData(finalData, finalData+finalDataPos);

	delete[] finalData;

	return outputData;
}

//CRC functions, copypasta from W3 PNG spec.

/* Table of CRCs of all 8-bit messages. */
unsigned long crc_table[256];

/* Flag: has the table been computed? Initially false. */
int crc_table_computed = 0;

/* Make the table for a fast CRC. */
void make_crc_table(void)
{
 unsigned long c;
 int n, k;

 for (n = 0; n < 256; n++) {
   c = (unsigned long) n;
   for (k = 0; k < 8; k++) {
     if (c & 1)
       c = 0xedb88320L ^ (c >> 1);
     else
       c = c >> 1;
   }
   crc_table[n] = c;
 }
 crc_table_computed = 1;
}

/* Update a running CRC with the bytes buf[0..len-1]--the CRC
  should be initialized to all 1's, and the transmitted value
  is the 1's complement of the final running CRC (see the
  crc() routine below)). */

unsigned long update_crc(unsigned long crc, unsigned char *buf, int len)
{
	unsigned long c = crc;
	int n;

	if (!crc_table_computed)
		make_crc_table();
	for (n = 0; n < len; n++)
	{
		c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
	}
	return c;
}

unsigned long format::CalculateCRC(unsigned char * data, int len)
{
	return update_crc(0xffffffffL, data, len) ^ 0xffffffffL;
}

ByteString format::URLEncode(ByteString source)
{
	ByteString result;
	for (auto it = source.begin(); it < source.end(); ++it)
	{
		if (!((*it >= 'a' && *it <= 'z') ||
		      (*it >= 'A' && *it <= 'Z') ||
		      (*it >= '0' && *it <= '9')))
		{
			auto byte = uint8_t(*it);
			result.append(1, '%');
			result.append(1, hex[(byte >> 4) & 0xF]);
			result.append(1, hex[ byte       & 0xF]);
		}
		else
		{
			result.append(1, *it);
		}
	}
	return result;
}

ByteString format::URLDecode(ByteString source)
{
	ByteString result;
	for (auto it = source.begin(); it < source.end(); ++it)
	{
		if (*it == '%' && it < source.end() + 2)
		{
			auto byte = uint8_t(0);
			for (auto i = 0; i < 2; ++i)
			{
				it += 1;
				auto *off = strchr(hex, tolower(*it));
				if (!off)
				{
					return {};
				}
				byte = (byte << 4) | (off - hex);
			}
			result.append(1, byte);
		}
		else if (*it == '+')
		{
			result.append(1, ' ');
		}
		else
		{
			result.append(1, *it);
		}
	}
	return result;
}
