#include <cstdarg>
#include <cstdlib>
#include "DocTemplate.h"
#include "Util.h"


std::string   DocTemplate::m_currentFile = "";
unsigned int  DocTemplate::m_currentLine = 0;


#define DebugMsg(fmt, ...)	DocTemplate::LogMessage(LL_Debug, fmt, ##__VA_ARGS__)
#define WarningMsg(fmt, ...)	DocTemplate::LogMessage(LL_Warning, fmt, ##__VA_ARGS__)
#define ErrorMsg(fmt, ...)	DocTemplate::LogMessage(LL_Error, fmt, ##__VA_ARGS__)


#ifndef _WIN32
#  define HAVE_CONSTEXPR
#else
#  include <windows.h>
#endif

#ifdef HAVE_CONSTEXPR

// TODO: XXX ### This side of HAVE_CONSTEXPR has not been tested
constexpr unsigned int hashString(const char* str, int h = 0)
{
	return !str[h] ? 5381 : (hashString(str, h + 1) * 33) ^ str[h];
}
unsigned int hashString(const std::string& str, int h = 0)
{
	return !str.c_str()[h] ? 5381 : (hashString(str, h + 1) * 33) ^ str.c_str()[h];
}

// Assumes that for lower case comparing, the values passed in are already lower case
#define Choose(x)       do { std::string tttmp = x; switch (hashString(x)) {
#define Option(x)       } case hashString(x): if ( tttmp == x ) {
#define OptionLower(x)  } case hashString(x): if ( str2lower(tttmp) == str2lower(std::string(x)) ) {
#define Defaults        } default: {
#define Done            } } while(false);

#else

#define Choose(x)       do { std::string tttmp = x; if (false)
#define Option(x)       } else if ( tttmp == x ) {
#define OptionLower(x)  } else if ( str2lower(tttmp) == str2lower(std::string(x)) ) {
#define Defaults        } else {
#define Done            } while(false);

#endif


/*

Rules:	Any extra white space is allowed, but if the the value, must be quoted,
eg:
		text = Blah         is okay
		text = Foo Bar      is not okay, must be quoted
		text = "Foo Bar"    is okay

		pos = 10,10         is okay
		pos = 10, 10        is not okay, must be quoted
		pos = "10, 10"      is okay

Removal of extra white space around the '=' is fine
eg:
		pos = 10,10         is okay
		pos= 10,10          is okay
		pos =10,10          is okay
		pos=10,10           is okay

Attributes must be deliminated by white space
eg:
		foo=val1 bar=val2   is okay
		foo=val1bar=val2    is not okay
		foo="val1"bar=val2  is not okay (but might work as you expect, but don't count on it)

*/


DocTemplate::DocTemplate()
{
	m_defaults.m_type = DIT_Unknown;
	m_defaults.m_bgColor = 0xffffff;
	m_defaults.m_pos.x = 10.0;
	m_defaults.m_pos.y = 10.0;
	m_defaults.m_size.x = 100.0;
	m_defaults.m_size.y = 100.0;
	m_defaults.m_font = "Helvetica";
	m_defaults.m_fontSize = 12.0f;
	m_defaults.m_fontColor = 0x000000;
	m_defaults.m_fillColor = 0x000000;
	m_defaults.m_penColor = 0x000000;
	m_defaults.m_penWidth = 1.0f;
	m_defaults.m_penStyle = PS_Solid;
	m_defaults.m_text = "undefined";
	m_defaults.m_imageFile = "undefined";
}


DocTemplate::~DocTemplate()
{
}


#ifndef _WIN32
int _vscprintf(const char * format, va_list pargs)
{
	int retval;
	va_list argcopy;
	va_copy(argcopy, pargs);
	retval = vsnprintf(NULL, 0, format, argcopy);
	va_end(argcopy);
	return retval;
}
#endif


void DocTemplate::LogMessage(yqLogLevel a_level, const char* a_msg, ...)
{
	va_list args;
	va_start(args, a_msg);
#ifdef _WIN32
	auto sizeForFileLineAndNull = m_currentFile.size() + 64 + 1;
	auto sizeRequired = _vscprintf(a_msg, args) + sizeForFileLineAndNull;
	auto a = (char*)_alloca(sizeRequired);
	int offset = _snprintf_s(a, sizeRequired, sizeForFileLineAndNull, "%s(%i): ", m_currentFile.c_str(), m_currentLine);
	vsnprintf_s(a + offset, sizeRequired - offset, INT_MAX - 1, a_msg, args);
	OutputDebugStringA(a);
	_freea(a);
#else
	vfprintf(stderr, a_msg, args);
#endif
	va_end(args);
	/*
	if (a_level == LL_Warning)
		DebugBreak();
	*/
	if (a_level == LL_Error)
		exit(-1);
}


std::vector<Point> str2pointList(const std::string& str)
{
	std::vector<Point> ret;
	std::vector<std::string> strs = split(str, ',');
	for (size_t i = 0; i < strs.size(); i+=2)
	{
		Point pnt = { 0, 0 };
		pnt.x = (float)atof(trim(strs[i + 0]).c_str());
		if ((i + 1) < strs.size())
			pnt.y = (float)atof(trim(strs[i + 1]).c_str());
		else
			WarningMsg("uneven coordinate count in shape, expecting a list of 2d coordinates so should be even count!\n");
		ret.push_back(pnt);
	}
	return ret;
}


Point str2pos(const std::string& str)
{
	Point ret = { 0, 0 };
	std::vector<std::string> strs = split(str, ',');
	ret.x = (float)atof(trim(strs[0]).c_str());
	ret.y = (float)atof(trim(strs[1]).c_str());
	return ret;
}


PenStyle str2style(const std::string& str)
{
	Choose(str)
	{
		OptionLower("Solid")
			return PS_Solid;
		OptionLower("Dashed")
			return PS_Dashed;
		Defaults
			WarningMsg("Bad style\n");
			break;
	}
	Done
	return PS_Solid;
}


Align str2align(const std::string& str)
{
	unsigned int alignment = 0;
	std::vector<std::string> strs = split(str, ',');
	std::string horzStr = trim(strs[0]);
	std::string vertStr = trim(strs[1]);

	Choose(horzStr)
	{
		OptionLower("left")   alignment |= AL_Left;    break;
		OptionLower("right")  alignment |= AL_Right;   break;
		OptionLower("top")    alignment |= AL_Top;     break;
		OptionLower("bottom") alignment |= AL_Bottom;  break;
		OptionLower("center") alignment |= AL_HCenter; break;
		OptionLower("centre") alignment |= AL_HCenter; break;
		Defaults
			WarningMsg("Bad horizontal alignment\n");
			break;
	}
	Done

	Choose(vertStr)
	{
		OptionLower("left")   alignment |= AL_Left;    break;
		OptionLower("right")  alignment |= AL_Right;   break;
		OptionLower("top")    alignment |= AL_Top;     break;
		OptionLower("bottom") alignment |= AL_Bottom;  break;
		OptionLower("center") alignment |= AL_VCenter; break;
		OptionLower("centre") alignment |= AL_VCenter; break;
		Defaults
			WarningMsg("Bad horizontal alignment\n");
		break;
	}
	Done

	// check we have a valid combination
	if ((alignment & (AL_Left | AL_HCenter | AL_Right)) && (alignment & (AL_Top | AL_VCenter | AL_Bottom)))
		return (Align)alignment;

	// for it to still be valid, then one of the values is a center, swap the center types and try again
	if (alignment & AL_HCenter)
		alignment = (alignment & ~AL_HCenter) | AL_VCenter;
	else if (alignment & AL_VCenter)
		alignment = (alignment & ~AL_VCenter) | AL_HCenter;

	if ((alignment & (AL_Left | AL_HCenter | AL_Right)) && (alignment & (AL_Top | AL_VCenter | AL_Bottom)))
		return (Align)alignment;

	WarningMsg("Bad alignment values\n");
	return (Align)(AL_Top | AL_Left);
}


void DocTemplate::ReadTemplateFile(const char* a_fileName)
{
	m_items.clear();
	FILE* f = 0;
	fopen_s(&f, a_fileName, "rb");
	if ( f )
	{
		m_currentFile = a_fileName;

		char lineBuf[1024];
		DocTemplateItem currentItem = m_defaults;
		bool first = true;
		while ( fgets(lineBuf, sizeof(lineBuf), f) ) {

			m_currentLine++;

			if (lineBuf[0] == '[') {
				// new item
				if (!first) {
					if (currentItem.m_type == DIT_Defaults)
						m_defaults = currentItem;
					else
						m_items.push_back(currentItem);
					currentItem = m_defaults;
				}
				first = false;

				Choose(trim(std::string(lineBuf)))
				{
					OptionLower("[Defaults]")
						currentItem.m_type = DIT_Defaults;
						break;

#define STR_SWITCH_CASE( OPT ) \
					OptionLower("[" #OPT "]") \
						currentItem.m_type = DIT_##OPT; \
						break;
					
					OptionLower("[Text]") // Alias for Label
						currentItem.m_type = DIT_Label;
						break;
					STR_SWITCH_CASE(Label)
					STR_SWITCH_CASE(Background)
					STR_SWITCH_CASE(Image)
					STR_SWITCH_CASE(Line)
					STR_SWITCH_CASE(Box)
					STR_SWITCH_CASE(Circle)
					STR_SWITCH_CASE(Ellipse)
					STR_SWITCH_CASE(Polygon)
					STR_SWITCH_CASE(Spline)
					STR_SWITCH_CASE(Shape)

					Defaults
						currentItem.m_type = DIT_Unknown;
						break;
				}
				Done

			} else {

				std::vector<std::string> strs = split(lineBuf, '=');
				std::string s = (strs.size() > 0) ? trim(strs[0]) : "";
				if (strs.size() != 2)
				{
					if (s != "")
						WarningMsg("Error parsing line, no '=' or line is whitespace.\n");
					continue;
				}

				Choose(s)
				{
					//OptionLower("id")
					//OptionLower("flags")// wrap, ignore, clip

					OptionLower("bgColor")
						currentItem.m_bgColor = str2col(trim(strs[1]));
						break;
					OptionLower("align")
						currentItem.m_alignment = str2align(trim(strs[1]));
						break;
					OptionLower("pos")
						currentItem.m_pos = str2pos(trim(strs[1]));
						break;
					OptionLower("size")
						currentItem.m_size = str2pos(trim(strs[1]));
						break;
					OptionLower("font")
						currentItem.m_font = trim(strs[1]);
						break;
					OptionLower("fontSize")
						currentItem.m_fontSize = str2float(trim(strs[1]));
						break;
					OptionLower("color") // alias for all the foreground colors
						currentItem.m_fontColor = str2col(trim(strs[1]));
						currentItem.m_fillColor = str2col(trim(strs[1]));
						currentItem.m_penColor = str2col(trim(strs[1]));
						break;
					OptionLower("fontColor")
						currentItem.m_fontColor = str2col(trim(strs[1]));
						break;
					OptionLower("fillColor")
						currentItem.m_fillColor = str2col(trim(strs[1]));
						break;
					OptionLower("penColor")
						currentItem.m_penColor = str2col(trim(strs[1]));
						break;
					OptionLower("penSize") // alias for penWidth
						currentItem.m_penWidth = str2float(trim(strs[1]));
						break;
					OptionLower("penWidth")
						currentItem.m_penWidth = str2float(trim(strs[1]));
						break;
					OptionLower("penStyle")
						currentItem.m_penStyle = str2style(trim(strs[1]));
						break;
					OptionLower("text")
						currentItem.m_text = trim(strs[1]);
						break;
					OptionLower("points")
						currentItem.m_shape = str2pointList(trim(strs[1]));
						break;
					OptionLower("file")
						currentItem.m_imageFile = trim(strs[1]);
						break;
					Defaults
						if (s != "" && strs.size() != 1)
							currentItem.m_type = DIT_Unknown;
						break;
				}
				Done
			}

			/*
			Image ( pos={0,0} size={w,h} file="image.jpg" )
			Line ( pos={0,0} size={w,h} points={20,20,30,30,40,20} pen={"#000",1.4,"solid"} )
			Shape ( pos={0,0} size={w,h} points={20,20,30,30,40,20} fill="#fff" pen={"#000",1.4,"solid"} )
			//box, circle, ellipse, poly, spline, label, image
			*/
		}
		if (currentItem.m_type == DIT_Defaults)
			m_defaults = currentItem;
		else
			m_items.push_back(currentItem);
		fclose(f);
	}
	else
	{
		WarningMsg("Problem opening file\n");
	}
}



void DocTemplate::Apply(DocOutputPage& page)
{
	for (size_t i = 0; i < m_items.size(); ++i)
	{
		if (m_items[i].m_type == DIT_Label)
		{
			float x = m_items[i].m_pos.x;
			float y = m_items[i].m_pos.y;
			if (x < 0.0) x = x + page.width();   // negative numbers are aligned from the right
			if (y < 0.0) y = y + page.height();  // negative numbers are aligned from the bottom
			page.setFontSize(0, m_items[i].m_fontSize); // TODO: map font name to a font index or make the output accept a string
			page.setColor(m_items[i].m_fontColor);
			page.drawText(x, y, m_items[i].m_text.c_str());
		}
	}
    page.drawImage("test/letterhead.png", 0, 0, page.width(), page.height());
}


void DocTemplate::WriteTemplateFile(const char* /*a_fileName*/)
{
}

