#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#include <locale>

#include "DocConvert.h"
#include "DocVisitor.h"
#include "DocTemplate.h"
#include "DocOutput.h"
#include "DocSVG.h"
#include "Util.h"

#include "html.h"
#include "tinyxml.h"

#ifndef _WIN32
#  include <unistd.h>
#else
   extern "C" void __stdcall Sleep(unsigned int);
#endif


#define DEF_IUNIT        1024
#define DEF_OUNIT        64
#define DEF_MAX_NESTING  16


void RemoveFile(const char* fileName)
{
#ifdef _WIN32
    FILE* tmpF = 0;
    bool first = true;
    do
    {
        fopen_s(&tmpF, fileName, "rb");
        if (tmpF)
        {
            fclose(tmpF);
            _unlink(fileName);
            ::Sleep(100);
            if (first)
                printf("waiting for output file to be removed.");
            else
                printf(".");
            first = false;
            fflush(stdout);
        }
    } while (tmpF != 0);
#else
    unlink(fileName);
#endif
}


/*
fseek(f, 0L, SEEK_END);
long fileSize = ftell(f);
fseek(f, 0L, SEEK_SET);
uint8_t* inputBuffer = (uint8_t*)malloc(fileSize);
size_t inputBufferSize = fread(inputBuffer, 1, fileSize, f);
*/


hoedown_buffer *ReadInWholeFile(const char* inputFileName)
{
    // Read in the markdown file
    FILE* f = 0;
    fopen_s(&f, inputFileName, "rt"); // text or binary? Can it be utf8, and if so, do I need to read in binary mode?
    if (!f)
        return 0;
    hoedown_buffer *ib = hoedown_buffer_new(DEF_IUNIT);
    if (hoedown_buffer_putf(ib, f))
        fprintf(stderr, "I/O errors found while reading input.\n");
    fclose(f);
    return ib;
}


hoedown_buffer *ConvertMarkdownToHTML(uint8_t* inputBuffer, size_t inputBufferSize)
{
    hoedown_html_flags flags = (hoedown_html_flags)(HOEDOWN_HTML_ESCAPE | HOEDOWN_HTML_HARD_WRAP | HOEDOWN_HTML_USE_XHTML);
    hoedown_extensions ext = (hoedown_extensions)(HOEDOWN_EXT_FOOTNOTES | HOEDOWN_EXT_SPACE_HEADERS | HOEDOWN_EXT_FENCED_CODE | HOEDOWN_EXT_TABLES);
    hoedown_renderer *renderer = hoedown_html_renderer_new(flags, 0);
    hoedown_buffer *ob = hoedown_buffer_new(DEF_OUNIT);
    hoedown_document *document = hoedown_document_new(renderer, ext, DEF_MAX_NESTING);
    hoedown_document_render(document, ob, inputBuffer, inputBufferSize);
    hoedown_document_free(document);
    hoedown_html_renderer_free(renderer);
    return ob;
}


void SVGTest(const char* a_fileName, double scale, DocOutputPage* outputPage)
{
    hoedown_buffer* inputBuffer = ReadInWholeFile(a_fileName);
    if (!inputBuffer)
        return;
    // SVG xml parse
    TiXmlDocument parser;
    parser.Parse((char*)inputBuffer->data);
    DocSVG visitor(scale);
    parser.Accept(&visitor);
    visitor.DumpOperations();
    visitor.WriteTo(outputPage);
    hoedown_buffer_free(inputBuffer);
}


void ConvertHTMLToPDF(const char* templateFileName, uint8_t* inputBuffer, DocOutputDevice* outputDoc)
{
    // xml parse
    DocStyle style;
    DocTemplate templ;
    TiXmlDocument parser;

    templ.ReadTemplateFile(templateFileName);// "test.tmpl");
    parser.Parse((char*)inputBuffer, 0, TIXML_ENCODING_UTF8);
    DocVisitor visitor(outputDoc, &style, &templ);
    parser.Accept(&visitor);

    if (visitor.needsPageCount())
    {
      TiXmlDocument parser2;
      parser2.Parse((char*)inputBuffer, 0, TIXML_ENCODING_UTF8);
      outputDoc->reset();
      DocVisitor visitor2(outputDoc, &style, &templ, visitor.pageCount());
      parser2.Accept(&visitor2);
    }

    //SVGTest("test/triangle.svg", 0.02, outputDoc);
    //SVGTest("test/ArcTest.svg", 1.0, outputDoc);
    //SVGTest("../templates/bars.svg", 0.02, outputDoc);    
}


void SaveHTML(const char* fileName, const uint8_t* data, size_t dataSize)
{
    RemoveFile(fileName);
    FILE* f = 0;
    fopen_s(&f, fileName, "wb");
    if (!f)
        return;

    const char* XMLType = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    const char *CSSText = "<html><head><title>test</title><link rel=\"stylesheet\" type=\"text/css\" href=\"base.css\"></head><body>";
    fwrite(XMLType, 1, strlen(XMLType), f);
    fwrite(CSSText, 1, strlen(CSSText), f);
    fwrite(data, 1, dataSize, f);
    fclose(f);
}


struct DocConvert::Pimpl
{
    Pimpl(void* errorHandler)
      : doc((HPDF_Error_Handler)errorHandler)
    {
    }
    ~Pimpl() {
        if (inputBuffer)
            hoedown_buffer_free(inputBuffer);
        if (outputBuffer)
            hoedown_buffer_free(outputBuffer);
    }
    DocOutputDevice doc;
    hoedown_buffer* inputBuffer;
    hoedown_buffer* outputBuffer;
    std::string     m_title;
};


DocConvert::DocConvert(void* errorHandler) : m_pimpl(std::make_unique<Pimpl>(errorHandler))
{
    m_pimpl->inputBuffer = 0;
    m_pimpl->outputBuffer = 0;
}


DocConvert::~DocConvert()
{
}


void DocConvert::SetTitle(const char* title)
{
    m_pimpl->m_title = title;
}


void DocConvert::SetSource(const char* inFileName)
{
    if (m_pimpl->inputBuffer)
        hoedown_buffer_free(m_pimpl->inputBuffer);
    m_pimpl->inputBuffer = ReadInWholeFile(inFileName);
    ReplaceMetadata();
}


void DocConvert::SetSourceData(const char* inData, size_t a_size)
{
    if (m_pimpl->inputBuffer)
        hoedown_buffer_free(m_pimpl->inputBuffer);
    m_pimpl->inputBuffer = hoedown_buffer_new(DEF_IUNIT);
    hoedown_buffer_put(m_pimpl->inputBuffer, (const uint8_t*)inData, a_size);
    ReplaceMetadata();
}


//! returns the offset where the metadata ends (where the rest of the doc starts)
// static
int DocConvert::ExtractMetadataValues(size_t inputSiz, const char* inputData, MetaData& outputMetaData)
{
    int i = 1;
    const size_t siz = inputSiz;
    const char* data = inputData;
    bool foundMeta = false;
    bool foundStartOfMetaMark = false;
    if (siz > 4)
    {
      if (data[i-1] == '-' && data[i] == '-' && data[i+1] == '-' && data[i+2] == '\n')
      {
        foundStartOfMetaMark = true;
        i += 4;
      }
    }
    while (i < siz)
    {
      int startOfMetaKey = i-1;
      if (isalnum(data[i-1]))
      {
        i++;
        while (isalnum(data[i-1]) || data[i-1] == '_' || data[i-1] == '-')
        {
          i++;
        }
        if (data[i-1] == ':')
        {
          i++;
          int endOfMetaKey = i-2;
          while (data[i-1] == ' ')
            i++;
          int endOfRawMetaKey = i - 1;
          int startOfMetaValue = i-1;
          while (data[i-1] != '\n' || data[i] == ' ')
            i++;
          int endOfMetaValue = i-1;
          std::string key(data+startOfMetaKey, endOfMetaKey - startOfMetaKey);
          std::string rawKey(data+startOfMetaKey, endOfRawMetaKey - startOfMetaKey);
          std::string value(data+startOfMetaValue, endOfMetaValue- startOfMetaValue);
          std::string keyLower;
          keyLower.resize(key.size());
          std::transform(key.begin(), key.end(), keyLower.begin(), ::tolower);

          // convert all whitespace characters to spaces
          //std::transform(value.begin(), value.end(), value.begin(), [](const char& ch) { return (std::isspace(ch)) ? ' ' : ch; });
          // compress whitespace
          value.erase(std::unique(value.begin(), value.end(), [](char lhs, char rhs){ return (lhs == ' ' || lhs == '\n') && (rhs == ' '); }), value.end());
          value = trim(value);

          outputMetaData.AddValue(keyLower, rawKey, value);
        }
        else
        {
          // Not a meta-key, rewind to where we started parsing the previous line
          i = startOfMetaKey - 2;
          break;
        }
      }
      if (data[i-1]=='\n' && data[i]=='\n')
      {
        break;
      }
      i++;
    }

    //printf("---\n");
    for (auto kp : outputMetaData.map)
    {
      //printf("meta kvp, -%s- = -%s-\n", kp.first.c_str(), kp.second.c_str());
    }

    if (i > siz || outputMetaData.map.empty())
      i = 0;

    return i;
}


void DocConvert::ReplaceMetadata()
{
    const size_t siz = m_pimpl->inputBuffer->size;
    const char* data = (const char*)m_pimpl->inputBuffer->data;

    int i = ExtractMetadataValues(siz, data, m_metaData);

    const size_t newSiz = siz - i;
    const char* newData = (char*)data + i;

    std::vector<char> newBuf;
    newBuf.reserve(newSiz + 1024);

    struct Heading { int depth; std::string text; };
    std::vector<Heading> tableOfContents;
    size_t toc_offset = -1;
    int curHeadingDepth = 0;
    bool firstHeading = true;
    int firstHeadingDepth = 0;

    for (int i = 0; i < newSiz; i++)
    {
      int curI = i;
      if (newData[i] == '[' && (i+1) < newSiz && newData[i+1] == '%')
      {
        // Looks like meta-expansion
        int startOfToken = i+2;
        while (newData[i] != ']')
        {
          i++;
        }
        int endOfToken = i;
        std::string key(newData+startOfToken, endOfToken - startOfToken);
        std::string keyLower;
        keyLower.resize(key.size());
        std::transform(key.begin(), key.end(), keyLower.begin(), ::tolower);
        if (m_metaData.map.count(keyLower))
        {
          std::string val = m_metaData.map[keyLower].value;
          //printf("found token -%s-\n", key.c_str());
          for (char ch : val)
            newBuf.push_back(ch);
          i++;
        }
        else
        {
          if (keyLower == "toc")
            toc_offset = newBuf.size();
          i = curI + 1;
        }
      }
      if (toc_offset != -1 && newData[i] == '\n' && newData[i+1] == '#')
      {
        curI = i;
        int hashes = 0;
        while (newData[i+1] == '#')
        {
          hashes++;
          i++;
        }
        if (firstHeading)
        {
          firstHeading = false;
          firstHeadingDepth = hashes;
        }
        int startOfToken = i+1;
        while (newData[i+1] != '#' && newData[i+1] != '\n')
        {
          i++;
        }
        int endOfToken = i + 1;
        std::string heading(newData+startOfToken, endOfToken - startOfToken);
        tableOfContents.push_back({ hashes, heading });
        i = curI;
      }
      newBuf.push_back(newData[i]);
    }
    if (toc_offset != -1)
    {
      int smallestDepth = 6;
      for (auto& heading : tableOfContents)
        if (heading.depth < smallestDepth)
          smallestDepth = heading.depth;

      std::string tocString;
      for (auto& heading : tableOfContents)
      {
        std::string text = "- " + heading.text;
        int hashes = heading.depth - smallestDepth;
        while (hashes)
        {
          text = "   " + text;
          hashes--;
        }
        tocString += text + "\n";
      }

      std::string dummyHeading = "- ";
      std::string prepend = "### Table of Contents\n";
      for (int d = 1; d < firstHeadingDepth - smallestDepth; d++)
      {
        prepend += dummyHeading + "\n";
        dummyHeading = "   " + dummyHeading;
      }
      tocString = prepend + tocString;
//      tableOfContents = prepend + tableOfContents;

      std::vector<char> newBuf2;
      newBuf2.reserve(newBuf.size() + tocString.size() + 1024);

      for (int i = 0; i < toc_offset; i++)
        newBuf2.push_back(newBuf[i]);
      for (int i = 0; i < tocString.size(); i++)
        newBuf2.push_back(tocString[i]);
      for (int i = toc_offset + 5; i < (newBuf.size()); i++)
        newBuf2.push_back(newBuf[i]);

      newBuf = newBuf2;
    }
    if (m_pimpl->inputBuffer)
        hoedown_buffer_free(m_pimpl->inputBuffer);
    m_pimpl->inputBuffer = hoedown_buffer_new(DEF_IUNIT);
    hoedown_buffer_put(m_pimpl->inputBuffer, (const uint8_t*)newBuf.data(), newBuf.size());
}


void DocConvert::Convert()
{
    if (!m_pimpl->inputBuffer)
        return;
    m_pimpl->outputBuffer = ConvertMarkdownToHTML(m_pimpl->inputBuffer->data, m_pimpl->inputBuffer->size);
}


void DocConvert::GetHTMLData(char** data, size_t* size)
{
    *data = (char*)m_pimpl->outputBuffer->data;
    *size = m_pimpl->outputBuffer->size;
}


void DocConvert::OutputHTML(const char* outFileName)
{
    SaveHTML(outFileName, m_pimpl->outputBuffer->data, m_pimpl->outputBuffer->size);
}


// static
std::string DocConvert::CryptPassword(const std::string& password)
{
  if (!password.empty())
  {
    std::string pwOneTimePad = "WickedDocs";
    while (password.size() > pwOneTimePad.size())
      pwOneTimePad += pwOneTimePad;
    std::string pw;
    for (int i = 0; i < password.size(); i+=2)
    {
      std::string str;
      str += password[i+0];
      str += password[i+1];
      //  printf(" hex code: -%s-\n", str.c_str());
      try {
        char c = char(std::stoul(str, nullptr, 16) ^ pwOneTimePad[i/2]);
        pw += c;
        // printf(" hex decode: -0x%x-\n", c);
      }
      catch (std::exception)
      {
        // printf("no good\n");
      }
    }
    return pw;
    //password = pw;
    // printf(" text-password = -%s-\n", password.c_str());
  }
  return password;
}


void DocConvert::OutputPDF(const char* templateFileName, const char* outFileName, bool preview)
{
    //RemoveFile(outFileName);
    m_pimpl->doc.setAuthor(m_metaData.map["author"].value);
    m_pimpl->doc.setTitle(m_metaData.map["title"].value, m_metaData.map["subtitle"].value);
    if (!preview)
      m_pimpl->doc.setPassword(CryptPassword(m_metaData.map["password"].value));
    ConvertHTMLToPDF(templateFileName, m_pimpl->outputBuffer->data, &m_pimpl->doc);
    m_pimpl->doc.finalize(outFileName);
}


void DocConvert::GetPDFData(char* data, size_t* size)
{
    m_pimpl->doc.finalize(data, size);
}

