#include <cstdlib>
#include <cstdio>
#include <stdio.h>
#include <libxml/parser.h>

#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>

#include <sys/stat.h>

#include <fstream>
#include <string>
#include <ctime>
using namespace std;

#include "fileUtils.h"

#define MY_ENCODING "ISO-8859-1"

uint8_t writeToErrorLog (const string & postStr, int numTabs)
{
  //returns true upon success.
  return writeToLog (postStr, numTabs, LOG_ERROR_INDEX);
}

uint8_t writeToLog (const string & postStr, int numTabs, int logIndex)
{
  //returns true upon success.
  ofstream fileOutput;
  time_t t;
  char * timeFormat;
  struct tm * timeinfo;
  uint32_t i;
  string str;
  char indexStr [20];

  sprintf (indexStr, "%u", logIndex);

  switch (logIndex)
    {
    case (LOG_ERROR_INDEX):
      str.assign("log/error.log");
      break;
    default:
      str.erase ();
      str = "Unknown log index ";
      str += indexStr;
      str += ", defaulting to error log.";
      writeToLog (str, numTabs, LOG_ERROR_INDEX);
      str.assign("log/error.log");
      break;
    };

  fileOutput.open(str.c_str(), ios_base::app);

  if (!fileOutput)
    return false;

  time (&t); //stores the current time into t
  timeinfo = localtime (&t);
  timeFormat = asctime(timeinfo);
  str.assign (timeFormat);
  i = str.find_last_of('\n');
  if (i != string::npos)
    {
      str.erase (i, 1);
    }

  fileOutput << "[" << str << "] ";

  for (i = 0; i < numTabs; i++)
    {
      fileOutput << "\t";
    }

  fileOutput << postStr.c_str();
  fileOutput << endl;

  fileOutput.close ();
  return true;
}

ofstream * saveToRecentSlotFstream ()
{
  //deletes the temporary "recent" file and returns a pointer to it.
  ofstream * recentFile;

  recentFile = new ofstream;

  recentFile -> open("saves/recent.sav", ios_base::trunc);

  if (recentFile == NULL)
    return NULL;

  return recentFile;
}

void closeRecentSlotFstream (ofstream * file)
{
  //closes the "recent" file

  file -> close ();
}

void deleteFile (uint8_t fileIndex)
{
  //deletes a file
  char indexStr [20];
  struct stat stFileInfo;
  int intStat;

  sprintf (indexStr, "saves/%u.sav", fileIndex);
  intStat = stat (indexStr, &stFileInfo);

  if (intStat == 0)
    {
      //file exists. Delete it.
      remove (indexStr);
    }
  else
    {
      //file doesn't exist. Do nothing.
    }
}

void renameFile (uint8_t fileIndex)
{
  //renames the recent file
  char indexStr [20];

  deleteFile (fileIndex);

  sprintf (indexStr, "saves/%u.sav", fileIndex);

  rename ("saves/recent.sav", indexStr);
}

xmlTextWriterPtr saveToRecentSlotXML ()
{
  //deletes the temporary "recent" file and returns a pointer to it.
  xmlTextWriterPtr writ;

  remove ("saves/recent.sav");

  /* Create a new XmlWriter for uri, with no compression. */
  writ = xmlNewTextWriterFilename("saves/recent.sav", 0);

  if (writ == NULL) {
    writeToErrorLog ("SaveToRecentSlotXML: Could not create xml writer", 0);
    return NULL;
  }

  /*
  if (xmlTextWriterStartDocument(writ, NULL, MY_ENCODING, NULL) < 0) {
    writeToErrorLog ("SaveToRecentSlotXML: Could not start document", 0);
    return NULL;
  }
  */
  return writ;
}

void closeRecentSlotXML (xmlTextWriterPtr & writer)
{
  //closes the "recent" file

  //auto-close remaining elements
  if (xmlTextWriterEndDocument(writer) < 0)
    {
      writeToErrorLog ("CloseRecentSlotXML: Could not end document", 0);
      return;
    }
  xmlFreeTextWriter(writer);
}

bool loadXMLFile (xmlDocPtr & doc, const string & filename)
{
  //loads an xml file. cur points to the root of the document. doc points to the document itself.
  //errors are written to the log file.

  doc = xmlReadFile (filename.c_str(), NULL, 0);

  if (doc == NULL)
    {
      writeToErrorLog ("Failed to load xml file "+filename, 0);
      return false;
    }
  return true;
}

bool xPathExists (xmlDocPtr & doc, xmlXPathContextPtr & xpathCtx, const string & xpath1, const string & xpath2, const string & xpath3)
{
  //Sees if the XPath exists in the given document. Useful if you don't care about the results.
  xmlXPathObjectPtr xpathObj;
  xmlChar * xpathExpr;

  string tempStr=xpath1+xpath2+xpath3;
  xpathExpr=(xmlChar*)tempStr.c_str();

  xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
  if(xpathObj == NULL || xpathObj->nodesetval==NULL) {
    return false;
  }

  if (xpathObj->nodesetval->nodeNr==0)
    {
      xmlXPathFreeObject(xpathObj);
      return false;
    }

  xmlXPathFreeObject(xpathObj);
  return true;
}

xmlChar * getFirstElementUsingXPath (xmlDocPtr & doc, xmlXPathContextPtr & xpathCtx, const string & xpath1, const string & xpath2, const string & xpath3)
{
  //This uses XPaths to find the first appearance of an element and extracts its data.
  //This needs the context and up to 3 xpaths.
  //Whoever calls this is responsible for using xmlFree on the return value.
  //If there are errors (most likely invalid XPath or data cannot be found), returns NULL.

  xmlXPathObjectPtr xpathObj;

  xpathObj = getElementsUsingXPath (doc, xpathCtx, xpath1, xpath2, xpath3);

  if (xpathObj==NULL)
      return NULL;

  xmlChar *keyword;
  keyword = xmlNodeListGetString(doc, xpathObj->nodesetval->nodeTab[0]->xmlChildrenNode, 1);

  xmlXPathFreeObject(xpathObj);
  return keyword;
}

xmlXPathObjectPtr getElementsUsingXPath (xmlDocPtr & doc, xmlXPathContextPtr & xpathCtx, const string & xpath1, const string & xpath2, const string & xpath3)
{
  //This uses XPaths to find all appearances of an element. The caller is reponsible for going through the array and extracting elements.
  //This needs the context and up to 3 xpaths.
  //If there are errors (most likely invalid XPath or data cannot be found), returns NULL.
  //The user is responsible for calling xmlXPathFreeObject(xpathObj) to free the memory allocated by this function.

  xmlXPathObjectPtr xpathObj;
  xmlChar * xpathExpr;

  string tempStr=xpath1+xpath2+xpath3;
  xpathExpr=(xmlChar*)tempStr.c_str();

  xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
  if(xpathObj == NULL) {
    return NULL;
  }

  if(xmlXPathNodeSetIsEmpty(xpathObj->nodesetval)){
    xmlXPathFreeObject(xpathObj);
    return NULL;
  }

  return xpathObj;
}

//Tries to open the file.
//If the xpathstart is not blank, verifies the xmlpath exists.
//Returns true if the file is open and the path exists. doc and xpathCtx point to valid data.
//Returns false if the file does not open and the path doesn't exist. Data is freed in cases of failure.
bool prepareXMLFile (xmlDocPtr & doc, xmlXPathContextPtr & xpathCtx, const string & filename, const string & xpathStart)
{
  xmlXPathObjectPtr xpathObj;

  doc = xmlReadFile (filename.c_str(), NULL, 0);

  if (doc==NULL)
    {
      writeToErrorLog ("Failed to load xml file "+filename, 0);
      xmlFreeDoc(doc);
      return false;
    }

  xpathCtx = xmlXPathNewContext(doc);
  if(xpathCtx == NULL) {
    writeToErrorLog("Error: unable to create new XPath context for file "+filename,0);
    xmlFreeDoc(doc);
    return false;
  }

  //If the xpath is empty assume it works and return.
  if (xpathStart.empty())
    return true;

  //otherwise check to see if it loads
  xpathObj=getElementsUsingXPath (doc, xpathCtx, xpathStart, "", "");

  if (xpathObj==NULL)
    {
      //Failed to load shape. Cleanup and exit.
      xmlXPathFreeContext(xpathCtx);
      xmlFreeDoc (doc);
      writeToErrorLog ("XPath "+xpathStart+" does not exist in file "+ filename, 0);
      return false;
    }
  xmlXPathFreeObject(xpathObj);
  return true;
}

//tries to load an integer from the xml information given and load it into hash with the key name of keyName.
//returns true if successful.
//returns fale if it fails and puts defaultValue instead.
bool loadIntFromXMLAndPutIntoHash (xmlDocPtr & doc, xmlXPathContextPtr & xpathCtx, const string & xpath, const string & keyName, Param & hash, int defaultValue)
{
  int intToLoad;
  xmlChar * keyword;

  keyword = getFirstElementUsingXPath (doc, xpathCtx, xpath, "", "");
  if (keyword!=NULL)
    {
      intToLoad=(xmlXPathCastStringToNumber(keyword));
      xmlFree(keyword);
      hash.setInt(keyName, intToLoad);
      return true;
    }

  hash.setInt(keyName, defaultValue);
  return false;
}

//making a hash of options.
bool loadIntFromXMLAndPutIntoHash (xmlDocPtr & doc, xmlXPathContextPtr & xpathCtx,
                                   Param & outputHash, Param & hash)
{
  int intToLoad;
  xmlChar * keyword;
  bool response;

  string xpath="", keyName="NULL_INDEX";
  int defaultValue=0, randomizeStyle=RANDOMIZE_STYLE_NONE, randomizeMagnitude=0;

  hash.getString("X Path", xpath);
  hash.getString("Key Name", keyName);
  hash.getInt("Default Value", defaultValue);
  hash.getInt("Randomize Style", randomizeStyle);
  hash.getInt("Randomize Magnitude", randomizeMagnitude);

  keyword = getFirstElementUsingXPath (doc, xpathCtx, xpath, "", "");
  if (keyword!=NULL)
    {
      intToLoad=(xmlXPathCastStringToNumber(keyword));
      xmlFree(keyword);
      response=true;
    }
  else
    {
      intToLoad=defaultValue;
      response=false;
    }

  //randomize based on randomize style
  switch (randomizeStyle)
    {
    case(RANDOMIZE_STYLE_LINEAR_ADD):
      //randomly add [-randomizeMagnitude, randomizeMagnitude] to the amount.
      intToLoad += generateRandomNumber(-1 * randomizeMagnitude, randomizeMagnitude);
      break;
    case(RANDOMIZE_STYLE_PERCENTAGE):
      //modify the result so it's between [100%-randomizeMagnitude, 100%+randomizeMagnitude]
      intToLoad=intToLoad*generateRandomNumber(100-randomizeMagnitude, 100+randomizeMagnitude)/100;
      break;
    default:
      break;
    };

  outputHash.setInt(keyName, intToLoad);
  return response;
}
