#include <cstdarg>
#include <cstdlib>
#include <string>
#include <iostream>
#include "DocTemplate.h"
#include "Util.h"
#define HPDF_DEF_PAGE_WIDTH         595.276F
#define HPDF_DEF_PAGE_HEIGHT        841.89F


GENERATE_ENUM_SERIALIZATION_FUNCTIONS
(
 DocItemType,
 DIT_Last,
 DIT_Unknown,
 "Defaults",
 "Page",
 "Background",
 "Label",
 "Image",
 "Line",
 "Box",
 "Circle",
 "Ellipse",
 "Polygon",
 "Spline",
 "Shape",
 "Unknown"
)


GENERATE_ENUM_SERIALIZATION_FUNCTIONS
(
 TemplateRole,
 TR_Last,
 TR_Both,
 "CoverPage",
 "Document",
 "Both"
)


GENERATE_ENUM_SERIALIZATION_FUNCTIONS
(
 PenStyle,
 PS_Last,
 PS_Solid,
 "Solid",
 "Dashed"
)


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

/*

// auto means this will work with ints too (but depends how str2lower is defined)
// but means it won't work with const char*s.
#define strSwitch(x)    do { auto tttmp = x; if (false)
#define strCase(x)      } else if ( tttmp == x ) {
#define strLowerCase(x) } else if ( str2lower(tttmp) == str2lower(std::string(x)) ) {
#define strDefault      } else {
#define strSwitchEnd    } while(false);

*/


/*

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_defaultDefaults.m_type = DIT_Defaults;
	m_defaultDefaults.m_role = TR_Both;
	m_defaultDefaults.m_alpha = 1.0;
	m_defaultDefaults.m_bgColor = 0xffffff;
	m_defaultDefaults.m_pos.x = 10.0;
	m_defaultDefaults.m_pos.y = 10.0;
	m_defaultDefaults.m_size.x = 100.0;
	m_defaultDefaults.m_size.y = 100.0;
	m_defaultDefaults.m_font = "Helvetica";
	m_defaultDefaults.m_fontSize = 12.0f;
	m_defaultDefaults.m_fontColor = 0x000000;
	m_defaultDefaults.m_fillColor = 0x000000;
	m_defaultDefaults.m_penColor = 0x000000;
	m_defaultDefaults.m_penWidth = 1.0f;
	m_defaultDefaults.m_penStyle = PS_Solid;
	m_defaultDefaults.m_alignment = Align((int)AL_Left | (int)AL_Top);
	m_defaultDefaults.m_boundsAlignment = Align((int)AL_Left | (int)AL_Top);
  m_defaultDefaults.m_text = "undefined";
	m_defaultDefaults.m_imageFile = "undefined";
	m_defaults = m_defaultDefaults;
  m_pageTopLeft.x = 60.0;
  m_pageTopLeft.y = 120.0;
  m_pageBottomRight.x = HPDF_DEF_PAGE_WIDTH - 60.0;
  m_pageBottomRight.y = HPDF_DEF_PAGE_HEIGHT - 60.0;
  m_columns = 1;
  m_columnSpacing = 20.0;
}


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;
}
std::vector<Point> stringToPolygon(const std::string& str)
{
  return str2pointList(str);
}


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


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)
{
  if (str.empty())
  {
    return (Align)(AL_Left | AL_Top);
  }
	unsigned int alignment = 0;
	std::vector<std::string> strs = split(str, ',');
	std::string horzStr = trim(strs[0]);

  // if both are center, then one is hcenter and other is vcenter
  // if only one is center, we can determine if it is a hcenter or vcenter by the other value
  // we do this after first attempting to parse it

	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

  if (strs.size() < 2)
  {
    return (Align)(alignment | AL_Top);
  }
	std::string vertStr = trim(strs[1]);
	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 vertical 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);
}
Align stringToAlignment(const std::string& str)
{
  return str2align(str);
}


static std::string dirName(const std::string& fname)
{
  size_t pos = fname.find_last_of("\\/");
  return (std::string::npos == pos) ? "" : fname.substr(0, pos);
}


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;
    currentItem.m_type = DIT_Unknown;
		bool firstFirst = true;
		bool first = true;
		while ( fgets(lineBuf, sizeof(lineBuf), f) ) {

			m_currentLine++;

			if (lineBuf[0] == '[') {
		
        // First time through here we haven't read anything yet
        if (!firstFirst)
        {
          // new item - this is now after we have read one section and about to start to read a new section
          if (!first) {
            if (currentItem.m_type == DIT_Page)
            {
              m_pageTopLeft.x = currentItem.m_pos.x;
              m_pageTopLeft.y = currentItem.m_pos.y;
              m_pageBottomRight.x = m_pageTopLeft.x + currentItem.m_size.x;
              m_pageBottomRight.y = m_pageTopLeft.y + currentItem.m_size.y;
            }
            else if (currentItem.m_type == DIT_Defaults)
            {
              m_defaults = currentItem;
            }
            else
              m_items.push_back(currentItem);
            currentItem = m_defaults;
            currentItem.m_type = DIT_Unknown;
          }
          first = false;
        }
        firstFirst = 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(Page)
					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("role")
						currentItem.m_role = stringToTemplateRole(trim(strs[1]));
						break;
					OptionLower("alpha")
						currentItem.m_alpha = str2float(trim(strs[1]));
						break;
					OptionLower("bgColor")
						currentItem.m_bgColor = str2col(trim(strs[1]));
						break;
					OptionLower("align")
						currentItem.m_alignment = str2align(trim(strs[1]));
						break;
					OptionLower("item-align")
						currentItem.m_boundsAlignment = str2align(trim(strs[1]));
						break;
					OptionLower("pos")  // offset
						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("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;
					OptionLower("cspacing")
						m_columnSpacing = str2float(trim(strs[1]));
						break;
					OptionLower("columns")
						m_columns = stringToInt(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_Page)
    {
      m_pageTopLeft.x = currentItem.m_pos.x;
      m_pageTopLeft.y = currentItem.m_pos.y;
      m_pageBottomRight.x = m_pageTopLeft.x + currentItem.m_size.x;
      m_pageBottomRight.y = m_pageTopLeft.y + currentItem.m_size.y;
    }
    else if (currentItem.m_type == DIT_Defaults)
    {
      m_defaults = currentItem;
    }
    else
      m_items.push_back(currentItem);

    m_items.insert(m_items.begin(), m_defaults);

    fclose(f);
	}
	else
	{
		WarningMsg("Problem opening file : %s\n", a_fileName);
	}
}


void DocTemplate::Save()
{
  WriteTemplateFile(m_currentFile.c_str());
}


void SVGTest(const char* a_fileName, double scale, DocOutputPage* outputPage);


void DocTemplate::Apply(DocOutputPage& page)
{
	for (size_t i = 0; i < m_items.size(); ++i)
	{
    float x = m_items[i].m_pos.x;
    float y = m_items[i].m_pos.y;

    page.setFontSize(m_items[i].m_fontSize);
    page.setFillColor(m_items[i].m_fillColor);
    page.setPenColor(m_items[i].m_penColor);
    page.setPenWidth(m_items[i].m_penWidth);
    page.setPenStyle(m_items[i].m_penStyle);
    page.setAlpha(m_items[i].m_alpha);

    // TODO: handle special text like [PAGENUM], [PAGECOUNT] etc
		if (m_items[i].m_type == DIT_Label)
		{
      // positioning based on alignment flags
      //      - position based on relative point of the bounds of the text and of the page
      //
      //       eg: the bounds of text have a top,left,right,bottom,hcenter,vcenter values
      //           the screen has same, top,left,right,bottom,hcenter,vcenter values
      //
      //       The positioning is specified as offsets between these
      //       
      //       eg: position the top of the text 5 pixels below the top of the page
      //                        and the left of the text 5 pixels from the left of the page
      //
      //       or, the hcenter of the text at 0 pixels from the hcenter of the page etc.
      //
			
      // Must set the font properties first so that getting the textWidth works correctly
      page.setFontType(FT_Normal);         // TODO: map font name to a font index or make the output accept a string
      
      float pageW = page.width();
      float pageH = page.height();
      Align pageA = m_items[i].m_alignment;
      float pageX = (pageA & AL_Right)  ? pageW : ((pageA & AL_HCenter) ? pageW * 0.5f : 0.0f);
      float pageY = (pageA & AL_Bottom) ? pageH : ((pageA & AL_VCenter) ? pageH * 0.5f : 0.0f);

      float textW = page.textWidth(m_items[i].m_text.c_str());
      float textH = m_items[i].m_fontSize;
      Align textA = m_items[i].m_boundsAlignment;
      float textX = (textA & AL_Right)  ? textW : ((textA & AL_HCenter) ? textW * 0.5f : 0.0f);
      float textY = (textA & AL_Bottom) ? 0 : ((textA & AL_VCenter) ? -textH * 0.5f : -textH);

      x += pageX - textX;
      y += pageY - textY;

			//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.drawText(x, y, m_items[i].m_text.c_str());
		}
		if (m_items[i].m_type == DIT_Polygon)
    {
      page.drawPolygon((float*)m_items[i].m_shape.data(), m_items[i].m_shape.size());
    }
		if (m_items[i].m_type == DIT_Background)
    {
      std::string fileLocation = dirName(m_currentFile) + "/" + m_items[i].m_imageFile;
      if (endsWith(fileLocation, ".svg")) {
        SVGTest(fileLocation.c_str(), 0.02, &page);
      } else {
        // Draws image from top-left corner and stretched to the entire page
        page.drawImage(fileLocation.c_str(), 0, 0, page.width(), page.height());
      }
    }
		if (m_items[i].m_type == DIT_Image)
    {
      std::string fileLocation = dirName(m_currentFile) + "/" + m_items[i].m_imageFile;
      if (endsWith(fileLocation, ".svg")) {
        printf("Looks like an SVG -%s-\n", fileLocation.c_str());
        SVGTest(fileLocation.c_str(), 0.02, &page);
      } else {
        // Draw image at the given position and with the given size
        page.drawImage(fileLocation.c_str(), x, y, m_items[i].m_size.x, m_items[i].m_size.y);
      }
    }
	}

  page.setAlpha(1.0);
}


std::string stringFromAlignment(Align align)
{
  std::string ret;
  if (align & AL_Left)
    ret = "left";
  else if (align & AL_Right)
    ret = "right";
  else
    ret = "center";
  ret += ",";
  if (align & AL_Top)
    ret += "top";
  else if (align & AL_Bottom)
    ret += "bottom";
  else
    ret += "center";
  return ret;
}


std::string stringFromPoint(const Point& pnt)
{
  return stringFromFloat(pnt.x) + "," + stringFromFloat(pnt.y);
}


std::string stringFromPolygon(const std::vector<Point>& poly)
{
  std::string ret;
  bool first = true;
  for (const Point& pnt : poly)
  {
    if (!first)
      ret += ",";
    ret += stringFromPoint(pnt);
    first = false;
  }
  return ret;
}


bool operator==(const Point& pntA, const Point& pntB)
{
  return (pntA.x == pntB.x && pntA.y == pntB.y);
}


bool operator!=(const Point& pntA, const Point& pntB)
{
  return (pntA.x != pntB.x || pntA.y != pntB.y);
}


static void WriteItemToFile(FILE* f, const DocTemplateItem& item, const DocTemplateItem& defaults)
{
  fprintf(f, "[%s]\n", stringFromDocItemType(item.m_type).c_str());

#define OUTPUT_ITEM(member, tag, typ) \
  if (item.m_##member != defaults.m_##member) \
    fprintf(f, tag "=%s\n", stringFrom##typ(item.m_##member).c_str())

  OUTPUT_ITEM(role            , "role",        TemplateRole);
  OUTPUT_ITEM(alpha           , "alpha",       Float    );
  OUTPUT_ITEM(bgColor         , "bgColor",     Color    );
  OUTPUT_ITEM(pos             , "pos",         Point    );
  OUTPUT_ITEM(size            , "size",        Point    );
  OUTPUT_ITEM(font            , "font",        String   );
  OUTPUT_ITEM(fontSize        , "fontSize",    Float    );
  OUTPUT_ITEM(fontColor       , "fontColor",   Color    );
  OUTPUT_ITEM(fillColor       , "fillColor",   Color    );
  OUTPUT_ITEM(penColor        , "penColor",    Color    );
  OUTPUT_ITEM(penWidth        , "penWidth",    Float    );
  OUTPUT_ITEM(penStyle        , "penStyle",    PenStyle );
  OUTPUT_ITEM(alignment       , "align",       Alignment);
  OUTPUT_ITEM(boundsAlignment , "item-align",  Alignment);
  OUTPUT_ITEM(text            , "text",        String   );
  OUTPUT_ITEM(shape           , "points",      Polygon  );

  if (item.m_imageFile != defaults.m_imageFile)
    fprintf(f, "file=%s\n", stringFromString(item.m_imageFile).c_str());

}


void DocTemplate::WriteTemplateFile(const char* a_fileName)
{
	FILE* f = 0;
	fopen_s(&f, a_fileName, "w");
	if ( f )
	{
    // WriteItemToFile(f, m_defaults, m_defaultDefaults);
		// m_currentFile = a_fileName;
    for (const auto& item : m_items)
    {
      if (item.m_type == DIT_Defaults)
        WriteItemToFile(f, item, m_defaultDefaults);
      else
        WriteItemToFile(f, item, m_defaults);
      fprintf(f, "\n");
    }

    DocTemplateItem pageItem;
    pageItem = m_defaults;
    pageItem.m_type = DIT_Page;
    pageItem.m_pos = m_pageTopLeft;
    pageItem.m_size.x = m_pageBottomRight.x - m_pageTopLeft.x;
    pageItem.m_size.y = m_pageBottomRight.y - m_pageTopLeft.y;
    WriteItemToFile(f, pageItem, m_defaults);
    fprintf(f, "columns=%s\n", stringFromInt(m_columns).c_str());
    fprintf(f, "cspacing=%s\n", stringFromFloat(m_columnSpacing).c_str());
    fprintf(f, "\n");

    fclose(f);
  }
}


