#include "DocVisitor.h"
#include "Util.h"


const float fontSizes[6] = { 12.0f, 30.0f, 24.0f, 18.0f, 14.0f, 10.0f };
const float lineSpacing = 1.5f;
const float spaceFromDotToText = 8.0f;
//const float pageLeftMargin = 60.0f;
//const float pageRightMargin = 60.0f;
//const float pageTopMargin = 120.0f;
//const float pageBottomMargin = 60.0f;


  // <blockquote>     - an indented section            - draw a thick vertical line and indent
  // <pre><code>      - code which is pre-formatted    - interpret white-space literally
  // <code>           - inline quoted text             - italics/highlight


/*
   HPDF_Page_SetTextRenderingMode(page, HPDF_FILL);
   HPDF_Page_SetTextRenderingMode(page, HPDF_STROKE);
*/


DocVisitor::DocVisitor(DocOutputDevice* doc, DocStyle* style, DocTemplate* templ, int pageCount) :
  m_doc(doc),
  m_page(0),
  m_style(style),
  m_templ(templ)
{
  // For pre/code tags, we need to preserve the white space in the contained text
  TiXmlBase::SetCondenseWhiteSpace(false);

  x = y = 0.0f;
  currentStyle.push(FT_Normal);
  currentHeadingLevel.push(0);
  insideCode = 0;
  insideLink = 0;	
  insidePreformatted = 0;
  insideBlockQuote = 0;
  needBlockQuoteBar = false;
  startOfListItem = false;
  insideTable = false;
  alignCenter = false;

  // reset
  FlushBufferedText();

  for (int i = 0; i < 32; i++)
    indentLevels[i] = 30.0f * i;
  indentLevel = 0;

  m_pageNum = 0;
  m_pageCount = pageCount;
  m_needPageCount = false;

  //m_pageLeftMarginX = 60.0f;
  //m_pageRightMarginX = 60.0f;
  //m_pageTopMarginY = 120.0f;
  //m_pageBottomMarginY = 60.0f;

  // defaults
#define HPDF_DEF_PAGE_WIDTH         595.276F
#define HPDF_DEF_PAGE_HEIGHT        841.89F

  Point topLeft;
  Point bottomRight;
  templ->PageBounds(topLeft, bottomRight);

  m_pageLeftMarginX = topLeft.x;
  m_pageTopMarginY = topLeft.y;
  m_pageRightMarginX = bottomRight.x;
  m_pageBottomMarginY = bottomRight.y;

  m_columnCount = 1;
  m_columnSpacing = 20.0;
  
  templ->Columns(m_columnCount, m_columnSpacing);
    
  m_columnNum = m_columnCount - 1;  // Need to start on last column to force an AddPage to happen
  m_truePageLeftMargin = m_pageLeftMarginX;

  float usableWidth = bottomRight.x - topLeft.x;
  m_columnWidth = (usableWidth - ((m_columnCount - 1) * m_columnSpacing)) / m_columnCount;

  m_pageLeftMarginX = m_truePageLeftMargin;
  m_pageRightMarginX = m_pageLeftMarginX + m_columnWidth;
}


DocVisitor::~DocVisitor()
{
  delete m_page; // calls endText
}


void DocVisitor::DrawCodeBlockRect()
{
/*
  // Output a grey-background to the code/preformatted section
  float fontSize = fontSizes[currentHeadingLevel.top()];
  m_page->setFillColor(0xB0F0B0); // light green
  m_page->drawRect(m_pageLeftMarginX, y + m_pageTopMarginY - fontSize,
      m_pageRightMarginX - m_pageLeftMarginX, lineCount * (fontSize * lineSpacing));
*/
}


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


void DocVisitor::AddPage() // TODO: rename to NextColumn or something like that
{
  /*
  if (insideCode)
  {
    // output the rectangle around it
    DrawCodeBlockRect();
  }
  */

  // Doing columns in here by having it count which column we are up to
  // and adjusting the left and right margins to make it seem like a thin page
  // That means that the m_page[Top|Left|Right|Bottom]Margin variables are
  // really the bounds for the margin, not the page so should rename these
  m_columnNum++;
  m_pageLeftMarginX += m_columnWidth + m_columnSpacing;
  if (m_columnNum >= m_columnCount)
  {
    m_columnNum = 0;
    m_pageLeftMarginX = m_truePageLeftMargin;

    m_pageNum++;
    if (m_page)
      m_style->adornOldPage(*m_page);
    delete m_page; // calls endText
    m_page = m_doc->newPage();

    // Re-calculate margin positions using the actual page size
    Point topLeft;
    Point bottomRight;
    m_templ->PageBounds(topLeft, bottomRight);
    // internally we used the defaults to save it, undo this
    float rightMargin = HPDF_DEF_PAGE_WIDTH - bottomRight.x;
    float bottomMargin = HPDF_DEF_PAGE_HEIGHT - bottomRight.y;
    // and redo the calc using the actual page size
    m_pageRightMarginX = m_page->width() - rightMargin;
    m_pageBottomMarginY = m_page->height() - bottomMargin;

    float usableWidth = m_pageRightMarginX - m_truePageLeftMargin;
    m_columnWidth = (usableWidth - ((m_columnCount - 1) * m_columnSpacing)) / m_columnCount;
    m_pageRightMarginX = m_pageLeftMarginX + m_columnWidth;

    m_style->adornNewPage(*m_page);
    m_templ->Apply(*m_page);
    
    printf("Start SVG\n");
    //SVGTest("../templates/bars.svg", 0.02, m_page);    
    //SVGTest("../templates/curve.svg", 0.02, m_page);    
    SVGTest("../test-gradient-stops.svg", 0.02, m_page);    
    printf("End SVG\n");
    
    m_page->setFillColor(0);
    m_page->setFontType(currentStyle.top());
    m_page->setFontSize(fontSizes[currentHeadingLevel.top()]);
    
    printf("End new page\n");
  }

  m_pageRightMarginX = m_pageLeftMarginX + m_columnWidth;

  /*
  if (insideCode)
  {
    codeStartX = 0;
    codeStartY = 0;
  }
  */
}


void DocVisitor::CheckIfEndOfPage()
{
  if ((y + m_pageTopMarginY) > m_pageBottomMarginY)
  {
    // Start a new page
    AddPage();
    y = 0.0f;
  }
}


/// Visit a document.
bool DocVisitor::VisitEnter( const TiXmlDocument& /*doc*/ )
{
  AddPage();
  return true;
}


/// Visit a document.
bool DocVisitor::VisitExit( const TiXmlDocument& /*doc*/ )
{
  if (m_page)
    m_style->adornOldPage(*m_page);
  delete m_page; // calls endText
  m_page = 0;
  return true;
}



/// Visit a declaration
bool DocVisitor::Visit(const TiXmlDeclaration& /*declaration*/)
{
  return true;
}


/// Visit a stylesheet reference
bool DocVisitor::Visit(const TiXmlStylesheetReference& /*stylesheet*/)
{
  return true;
}


/// Visit a comment node
bool DocVisitor::Visit( const TiXmlComment& /*comment*/ )
{
  return true;
}


/// Visit an unknown node
bool DocVisitor::Visit(const TiXmlUnknown& /*unknown*/)
{
  return true;
}


void DocVisitor::NewLine(float amount)
{
  x = 0.0f;
  float h = fontSizes[currentHeadingLevel.top()] * lineSpacing * amount;
 
  //needBlockQuoteBar = true;

  m_page->setPenWidth(5.0);
  m_page->setPenColor(0xE0E0E0);
  for (int i = 0; i < insideBlockQuote; i++)
  {
    x += 2.5;
    m_page->drawLine(m_pageLeftMarginX + x, m_pageTopMarginY + y - h * 0.3, m_pageLeftMarginX + x, m_pageTopMarginY + y + h - h * 0.3);
    x += 13.0;
  }
  m_page->setPenWidth(1.0);
  if (!insideBlockQuote)
  {
    m_page->setPenColor(0);
    m_page->setFillColor(0);
  }

  y += h;
  CheckIfEndOfPage();
  if (indentLevel)
    x += indentLevels[indentLevel] + spaceFromDotToText;
}


/// Visit an element.
bool DocVisitor::VisitEnter( const TiXmlElement& element, const TiXmlAttribute* firstAttribute )
{
  if (strcmp(element.ValueTStr().c_str(), "img") == 0)
  {
    std::string altText = "";
    std::string srcFile = "";
    while (firstAttribute)
    {
      if (firstAttribute->Name() == std::string("alt"))
      {
        altText = firstAttribute->Value();
      }
      else if (firstAttribute->Name() == std::string("src"))
      {
        srcFile = firstAttribute->Value();
      }
      firstAttribute = firstAttribute->Next();
    }
    if (!srcFile.empty() || !altText.empty())
    {
      //printf("image src = %s, alt = %s\n", srcFile.c_str(), altText.c_str());

      // TODO: this is hacky way to get relative path to where images might be - but if this is a new document the
      // user is editing and they haven't saved yet, how do we know which directory to look in?
      // Also perhaps we need to collect up a list of referenced images that are used so they can be copied around together
      // with the markdown file - idea of a bundle - or perhaps multiple docs refer to the same shared library of images
      // so perhaps copied to a specified images directory. Need user preferences for this.

      std::string fileLocation = "../Resources/Documents/" + srcFile;
 
      float w, h;
      bool valid = m_page->imageSize(fileLocation.c_str(), w, h);
      w *= 0.25;
      h *= 0.25;
      if (valid && w > 1.0 && h > 1.0)
      {
        float aspectRatio = w / h;
        int maxWidth = m_pageRightMarginX - m_pageLeftMarginX;
        int maxHeight = m_pageBottomMarginY - m_pageTopMarginY;

        // clamp image to page bounds, maintaining aspect ratio of the image
        if (w > maxWidth)
          w = maxWidth, h = maxWidth / aspectRatio;
        if (h > maxHeight)
          h = maxHeight, w = maxHeight * aspectRatio;

        // Need to check remaining height in page and start new page if not enough
        if ((y + m_pageTopMarginY + h) >= m_pageBottomMarginY)
        {
          AddPage();
          y = 0.0f;
        }

        if (alignCenter)
        {
          // center the image
          NewLine(0.5);
          x = (maxWidth - w) / 2;
        }

        m_page->drawImage(fileLocation.c_str(), x + m_pageLeftMarginX, y + m_pageTopMarginY, w, h);

        x = 0.0;
        y += h;
      }
      else
      {
        // TODO: draw a box around where the image would have been?
        OutputText(altText.c_str());
        NewLine();
      }

    }
  }
  else if (strcmp(element.ValueTStr().c_str(), "p") == 0)
  {
    if (indentLevel == 0)
      NewLine();
  }
  else if (strcmp(element.ValueTStr().c_str(), "ol") == 0)
  {
    orderedList = true;
    listItemIndex = 1;
    NewLine(0.5);
  }
  else if (strcmp(element.ValueTStr().c_str(), "ul") == 0)
  {
    orderedList = false;
    NewLine(0.5);
  }
  else if (strcmp(element.ValueTStr().c_str(), "li") == 0)
  {
    NewLine();
    if (indentLevel)
      x -= indentLevels[indentLevel] + spaceFromDotToText;
    indentLevel++;
    x += indentLevels[indentLevel] + spaceFromDotToText;
    if (orderedList)
    {
      std::string str = std::to_string(listItemIndex) + ".";
     
      x -= spaceFromDotToText;
      //DrawText(m_pageLeftMarginX + x, y + m_pageTopMarginY, str.c_str());
      OutputText(str.c_str());
      x += spaceFromDotToText;

      // TODO: this is not correct across multiple depths of lists - need to have an array of indexes for different list depths
      listItemIndex++;
    }
    else
    {
      // TODO: draw bullet according to style settings (this is hard-coded as a circle/dot bullet)
      m_page->setFillColor(0x0);
      m_page->drawCircle(m_pageLeftMarginX + x - spaceFromDotToText, y + m_pageTopMarginY - fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.25f, 2.0f);
    }
    startOfListItem = true;
  }
  else if (strcmp(element.ValueTStr().c_str(), "code") == 0)
  {
    if (insideCode == 0)
    {
      codeStartX = x;
      codeStartY = y;
      codeText = "";
    
      currentStyle.push(FT_Fixed);
    }
    insideCode++;
  }
  else if (strcmp(element.ValueTStr().c_str(), "a") == 0)
  {

    std::string srcFile = "";
    while (firstAttribute)
    {
      if (firstAttribute->Name() == std::string("href"))
      {
        srcFile = firstAttribute->Value();
      }
      firstAttribute = firstAttribute->Next();
    }
  
    float textHeight = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.75f;
    m_page->startLink(srcFile.c_str(), m_pageLeftMarginX + x, m_pageTopMarginY + y - textHeight);

    if (insideLink == 0)
      currentStyle.push(FT_Bold);
    insideLink++;
    m_page->setFillColor(0x0000FF); // blue
  }
  else if (strcmp(element.ValueTStr().c_str(), "h1") == 0)
    currentHeadingLevel.push(1), NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "h2") == 0)
    currentHeadingLevel.push(2), NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "h3") == 0)
    currentHeadingLevel.push(3), NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "h4") == 0)
    currentHeadingLevel.push(4), NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "h5") == 0)
    currentHeadingLevel.push(5), NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "strong") == 0)
    currentStyle.push(FT_Bold);
  else if (strcmp(element.ValueTStr().c_str(), "em") == 0)
    currentStyle.push(FT_Oblique);
  else if (strcmp(element.ValueTStr().c_str(), "hr") == 0)
  {
  }
  else if (strcmp(element.ValueTStr().c_str(), "pre") == 0)
  {
    currentStyle.push(FT_Fixed);
    insidePreformatted++;
    NewLine();
  }
  else if (strcmp(element.ValueTStr().c_str(), "br") == 0)
    NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "table") == 0)
  {
    tableData.clear();
    insideTable = true;
  }
  else if (strcmp(element.ValueTStr().c_str(), "thead") == 0)
  {
    // ignore
  }
  else if (strcmp(element.ValueTStr().c_str(), "tbody") == 0)
  {
    // ignore
  }
  else if (strcmp(element.ValueTStr().c_str(), "tr") == 0)
  {
    tableData.emplace_back();
  }
  else if (strcmp(element.ValueTStr().c_str(), "td") == 0 || strcmp(element.ValueTStr().c_str(), "th") == 0)
  {
    std::string styleStr = "";
    while (firstAttribute)
    {
      if (firstAttribute->Name() == std::string("style"))
      {
        styleStr = firstAttribute->Value();
        break;
      }
      firstAttribute = firstAttribute->Next();
    }
    TDAlign align = TDA_Left;
    if (!styleStr.empty())
    {
      if (styleStr == "text-align: center")
        align = TDA_Center;
      else if (styleStr == "text-align: right")
        align = TDA_Right;
      else if (styleStr == "text-align: left")
        align = TDA_Left;
      else
        printf("unexpected table data style: %s\n", styleStr.c_str());
    }
    tableData.back().emplace_back(TD{ align, "" });
  }
  else if (strcmp(element.ValueTStr().c_str(), "blockquote") == 0)
  {
    insideBlockQuote++;  // block quotes can be nested
    m_page->setPenColor(0xE0E0E0);
    m_page->setFillColor(0xA0A0A0);
  }
  else
  {
    return false;
  }
  return true;
}


/// Visit an element.
bool DocVisitor::VisitExit( const TiXmlElement& element )
{
  if (strcmp(element.ValueTStr().c_str(), "img") == 0)
  {
    //NewLine();
  }
  else if (strcmp(element.ValueTStr().c_str(), "p") == 0)
  {
    if (indentLevel == 0)
      NewLine();
  }
  else if (strcmp(element.ValueTStr().c_str(), "ol") == 0)
  {
    if (indentLevel == 0)
      NewLine();
    else
      NewLine(0.5);
  }
  else if (strcmp(element.ValueTStr().c_str(), "ul") == 0)
  {
    if (indentLevel == 0)
      NewLine();
    else
      NewLine(0.5);
  }
  else if (strcmp(element.ValueTStr().c_str(), "li") == 0)
  {
    indentLevel--;
    //NewLine();
  }
  else if (strcmp(element.ValueTStr().c_str(), "code") == 0)
  {
    insideCode--;
    if (insideCode == 0)
    {
      float fontSize = fontSizes[currentHeadingLevel.top()];
      float fh = fontSize * lineSpacing;
      const char* str = codeText.c_str();

      if (insidePreformatted)
      {
        // Count how many lines this code section has
        int lineCount = 0;
        for (int i = 0; str[i] != 0; i++ ) {
          if (str[i] == '\n') {
            lineCount++;
          }	
        }

        //DrawCodeBlockRect();

        // Output a grey-background to the code/preformatted section
        m_page->setPenWidth(1);
        m_page->setFillColor(0xF0F0F0); // light gray
        m_page->setPenColor(0xF0F0F0);  // light gray
        m_page->setAlpha(0.7);
        m_page->drawRect(m_pageLeftMarginX, y + m_pageTopMarginY - fontSize,
            m_pageRightMarginX - m_pageLeftMarginX, (lineCount + 1.5) * fh);

        // Output the code text
        m_page->setFontType(currentStyle.top());
        m_page->setFontSize(fontSize);
        m_page->setFillColor(0); // black
        m_page->setPenColor(0);  // black
        m_page->setAlpha(1.0);

        y += (fontSize * lineSpacing) * 0.75; // half of 1.5 a line-height border
     //   x += (fontSize * lineSpacing) * 0.75; // half of 1.5 a line-height border
      }

      if (!insidePreformatted)
      {
        //insideCode = 1;
        StartRedirect();
      }
      else
      {
        x += (fontSize * lineSpacing) * 1.0; // half a line-height border
      }

      std::string currentWord;
      int i = 0;
      while ( str[i] != 0 ) {
        if ( str[i] == ' ' || str[i] == '\t' || str[i] == '\n' ) {
          //char s[2] = { str[i], 0 };
          //currentWord += s;
          OutputCurrentWord(currentWord.c_str());
          // x -= m_page->textWidth(" ");
          if (str[i] == '\n') {
            NewLine();
            x += (fontSize * lineSpacing) * 1.0; // half a line-height border
          }	
          currentWord = "";
        }
        else
        {
          char s[2] = { str[i], 0 };
          currentWord += s;
        }
        i++;
      }
      if ( str[i] == 0 ) {
        OutputCurrentWord(currentWord.c_str());
        //x -= m_page->textWidth(" ");
      }

      if (!insidePreformatted)
      {
        // insideCode = 0;
        float x1, y1, x2, y2;
        RedirectBounds(x1, y1, x2, y2);

        // Output a grey-background to the code/preformatted section
        m_page->setPenWidth(1);
        m_page->setFillColor(0xF0F0F0); // light gray
        m_page->setPenColor(0xF0F0F0);  // light gray
        
        m_page->setAlpha(0.7);
        m_page->drawRect(x1 - 1.0, y1, x2-x1 + 2.0, y2-y1);
        m_page->setAlpha(1.0);
        /*
        //m_pageLeftMarginX, y + m_pageTopMarginY - fontSize, m_pageRightMarginX - m_pageLeftMarginX, lineCount * (fontSize * lineSpacing));

        // Output the code text
        //m_page->setFontType(currentStyle.top());
        //m_page->setFontSize(fontSize);
        */
        m_page->setFillColor(0); // black
        m_page->setPenColor(0);  // black

        EndRedirect();
      }
      else
      {
        NewLine();
      }
      currentStyle.pop();

      //m_page->setFillColor(0); // black
    }
  }
  else if (strcmp(element.ValueTStr().c_str(), "a") == 0)
  {
    insideLink--;
    if (insideLink == 0)
    {
      float textHeight = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.25f;
      m_page->endLink(m_pageLeftMarginX + x, m_pageTopMarginY + y + textHeight);
      m_page->setFillColor(0x000000); // black
      currentStyle.pop();
    }
  }
  else if (strcmp(element.ValueTStr().c_str(), "hr") == 0)
  {
    float h = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.75;
    float y0 = y + m_pageTopMarginY + h;
    m_page->setFillColor(0xF0F0F0); // light gray
    m_page->setPenColor(0xF0F0F0);  // light gray
    //m_page->drawRect(x1, y1, x2, y2);
    // m_page->drawLine(m_pageLeftMarginX, y0, m_pageRightMarginX, y0);
    m_page->drawRect(m_pageLeftMarginX, y0, m_pageRightMarginX - m_pageLeftMarginX, 3.0);
    m_page->setFillColor(0); // black
    m_page->setPenColor(0);  // black
    NewLine(1.5);
  }
  else if (strcmp(element.ValueTStr().c_str(), "h1") == 0) {
    currentHeadingLevel.pop();
    //NewLine();
  }
  else if (strcmp(element.ValueTStr().c_str(), "h2") == 0)
  {
    currentHeadingLevel.pop();
    //NewLine();
    /*
    x = 0.0;
    y += fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.5f;
    m_page->drawLine(m_pageLeftMarginX, y + m_pageTopMarginY, m_pageRightMarginX, y + m_pageTopMarginY);
    y += fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.5f;
    CheckIfEndOfPage();
    */
  }
  else if (strcmp(element.ValueTStr().c_str(), "h3") == 0)
    currentHeadingLevel.pop();//, NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "h4") == 0)
    currentHeadingLevel.pop();//, NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "h5") == 0)
    currentHeadingLevel.pop();//, NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "strong") == 0)
    currentStyle.pop();
  else if (strcmp(element.ValueTStr().c_str(), "em") == 0)
    currentStyle.pop();
  else if (strcmp(element.ValueTStr().c_str(), "pre") == 0)
  {
    insidePreformatted--;
    currentStyle.pop();
  }//	NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "br") == 0)
  {}//	NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "table") == 0)
  {
    insideTable = false;
    OutputTable();
  }
  else if (strcmp(element.ValueTStr().c_str(), "thead") == 0)
  {}//	NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "tbody") == 0)
  {}//	NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "tr") == 0)
  {}//	NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "td") == 0)
  {}//NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "th") == 0)
  {}//	NewLine();
  else if (strcmp(element.ValueTStr().c_str(), "blockquote") == 0)
  {
    insideBlockQuote--;
    if (!insideBlockQuote)
    {
      m_page->setPenColor(0);
      m_page->setFillColor(0);
    }
  }//	NewLine();
  else {
    return false;
  }
  return true;
}


void DocVisitor::RedirectBounds(float& x1, float& y1, float& x2, float& y2)
{
  x1 = std::numeric_limits<float>::max();
  y1 = std::numeric_limits<float>::max();
  x2 = std::numeric_limits<float>::min();
  y2 = std::numeric_limits<float>::min();
  float h = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.8;
  for (auto& t : redirectedText)
  {
    float w = m_page->textWidth(t.text.c_str());
    float _x1 = t.x;
    float _y1 = t.y - h * 0.75;
    float _x2 = _x1 + w;
    float _y2 = _y1 + h;
    if (_x1 < x1) x1 = _x1;
    if (_y1 < y1) y1 = _y1;
    if (_x2 > x2) x2 = _x2;
    if (_y2 > y2) y2 = _y2;
  }
}


float DocVisitor::WidestWordInText(std::string& str, bool& stretch)
{
  float maxW = 0.0;
  std::vector<std::string> words = split(str, ' ');
  stretch = false;
  if (words.size() >= 2)
  {
    if (words[0] == "<<" && words[words.size()-1] == ">>")
    {
      str = join(std::vector<std::string>(words.begin() + 1, words.end() - 1), " ");
      stretch = true;
    }
  }
  for (auto& word : words)
  {
    float w = m_page->textWidth(word.c_str());
    if (w > maxW)
      maxW = w;
  }
  return maxW;
}


/*
  struct ListState
  {
    bool      ordered;
    int       index;
  };
  struct TextNode
  {
    int          listLevel;
    ListState    listState[16]; // up to 16 nested list levels 
    bool         codeFlag;
    bool         linkFlag;
    bool         blockQuoteFlag;
    bool         italicsFlag;
    bool         boldFlag;
    bool         preFlag;
    bool         tableFlag;
    FontType     style;
    std::string  words;
  };
  float bufferedTextWidth;
  std::vector<TextNode> bufferedText;
*/


void DocVisitor::FlushBufferedText()
{

  bufferedTextWidth = 0.0;
  bufferedText.clear();
}


void DocVisitor::OutputTable()
{
  const float colSpacing = 15.0;
  const float rowSpacing = 2.0;

  float fontSize = fontSizes[0];
  m_page->setFontType(FT_Bold);
  m_page->setFontSize(fontSize);
  m_page->setFillColor(0x0);

  int colCount = 0;
  for (const auto& tr : tableData)
    if (tr.size() > colCount)
      colCount = tr.size();

  int stretchCount = 0;
  std::vector<float> colWidths;     // if no wrapping, what would be the widest width of text in the column
  std::vector<float> minColWidths;  // if wrapping, what would be the smallest width of the column without wrapping mid-word
  std::vector<bool> stretchCols;    // if wrapping, what would be the smallest width of the column without wrapping mid-word
  colWidths.reserve(colCount);
  for (int i = 0; i < colCount; i++)
  {
    colWidths.push_back(0.0);
    minColWidths.push_back(0.0);
    stretchCols.push_back(false);
  }

  for (auto& tr : tableData)
  {
    int col = 0;
    for (auto& td : tr)
    {
      bool stretch = false;
      float w = m_page->textWidth(td.str.c_str());
      if (w > colWidths[col])
        colWidths[col] = w;
      w = WidestWordInText(td.str, stretch);
      if (!stretchCols[col])
      {
        stretchCount += (stretch) ? 1 : 0;
        stretchCols[col] = stretch;
      }
      if (w > minColWidths[col])
        minColWidths[col] = w;
      col++;
    }
  }

  float totalWidth = 0.0;
  for (float w : colWidths)
    totalWidth += w + colSpacing;

  // See if this table is too wide and some columns will need to wrap text over several lines
  float spaceRemaining = m_pageRightMarginX - (m_pageLeftMarginX + totalWidth);
  if (spaceRemaining > 0.0 && stretchCount)
  {
    // the table is not full-width, lets see if any columns are stretched - divide the space left across these
    float spacePerStretchCol = spaceRemaining / stretchCount;
    printf("distributing space to cols: %f\n", spacePerStretchCol);
    for (int i = 0; i < colCount; i++)
    {
      if (stretchCols[i])
        colWidths[i] += spacePerStretchCol;
    }
  }
  float needToReduceBy = -spaceRemaining;
  while (needToReduceBy > 0.0)
  {
    // some lines need to wrap - start with the widest column first
    int col = 0;
    int widestCol = -1;
    float widest = 0.0;
    for (float w : colWidths)
    {
      if (w > widest)
      {
        // skip if already at it's min
        if (w != minColWidths[col])
        {
          widest = w;
          widestCol = col;
        }
      }
      col++;
    }

    // Stop if all columns are already reduced to their mins
    if (widestCol == -1)
      break;

    // halve the column width, or if that is too much, shink it to make the table fit
    float newColWidth = widest * 0.5;
    if (newColWidth > needToReduceBy)
    {
      newColWidth = widest - needToReduceBy;
    }
    // don't resize below what will cause words to be broken mid-word
    if (newColWidth < minColWidths[col])
    {
      newColWidth = minColWidths[col];
    }
    colWidths[widestCol] = newColWidth;
    needToReduceBy -= widest;
    needToReduceBy += newColWidth;
    // this will loop, halving the widest column until they all fit.
    // the theory is that the widest column if wrapped would gain the most
    // but after doing that and wrapping is happening, it will be okay to
    // have other columns wrap if they will be wrapping on same lines as other column
  }

  // recalc
  totalWidth = 0.0;
  for (float w : colWidths)
    totalWidth += w + colSpacing;

  // TODO: we have dropped all formatting inside the table - all text is normal text - need to somehow save the formatting and apply it
  float totalHeight = tableData.size() * (fontSize * lineSpacing + rowSpacing);

  NewLine();
  
  // Check if need to start a new page
  if ((y + m_pageTopMarginY + totalHeight) > m_pageBottomMarginY)
  {
    // Start a new page
    AddPage();
    y = 0.0f;
  }

  m_page->setFontType(FT_Bold);
  m_page->setFontSize(fontSize);
  m_page->setPenColor(0x98bc53);//B0F0B0);
  m_page->setFillColor(0x0);

  float startY = y;
  int row = 0;
  for (const auto& tr : tableData)
  {
    if (alignCenter)
    {
      x += ((m_pageRightMarginX - m_pageLeftMarginX) - totalWidth) * 0.5;
    }

    float y2 = m_pageTopMarginY + y - fontSize * lineSpacing + 3.0;
    m_page->drawLine(m_pageLeftMarginX + x, y2, m_pageLeftMarginX + x + totalWidth, y2);

    //m_page->setPenWidth(0);
    //m_page->setFillColor(0xB0F0B0); // light green

    m_page->setAlpha(0.7);
    m_page->setFillColor((row == 0) ? 0xD0D0D0 : ((row % 2) ? 0xF0F0F0 : 0xE8E8E8)); // light gray
    m_page->drawRect(m_pageLeftMarginX + x, y2, totalWidth, fontSize * lineSpacing + rowSpacing);
    m_page->setFillColor(0);
    m_page->setAlpha(1.0);

    int col = 0;
    for (const auto& td : tr)
    {
      // OutputText(str);
      float strW = m_page->textWidth(td.str.c_str());
      float colW = colWidths[col] + colSpacing;
      if ( td.align == TDA_Left )
        m_page->drawText(m_pageLeftMarginX + x + (colSpacing * 0.5), y + m_pageTopMarginY, td.str.c_str());
      else if ( td.align == TDA_Center )
        m_page->drawText(m_pageLeftMarginX + x + ((colW - strW) * 0.5), y + m_pageTopMarginY, td.str.c_str());
      else if ( td.align == TDA_Right )
        m_page->drawText(m_pageLeftMarginX + x + colW - strW - (colSpacing * 0.5), y + m_pageTopMarginY, td.str.c_str());
      x += colW;
      col++;
    }
    x = 0.0;
    y += fontSize * lineSpacing + rowSpacing;
    if (row == 0)
      m_page->setFontType(FT_Normal);
    row++;
  }
  if (alignCenter)
  {
    x += ((m_pageRightMarginX - m_pageLeftMarginX) - totalWidth) * 0.5;
  }
  float y2 = m_pageTopMarginY + y - fontSize * lineSpacing + 3.0;
  m_page->drawLine(m_pageLeftMarginX + x, y2, m_pageLeftMarginX + x + totalWidth, y2);

  // Draw the vertical lines
  x = 0.0;
  float y1 = m_pageTopMarginY + startY - fontSize * lineSpacing + 3.0;
  for (int i = 0; i < colCount; i++)
  {
    if (alignCenter)
    {
      x += ((m_pageRightMarginX - m_pageLeftMarginX) - totalWidth) * 0.5;
    }

    m_page->drawLine(m_pageLeftMarginX + x, y1, m_pageLeftMarginX + x, y1 + totalHeight);
    x += colWidths[i] + colSpacing;
  }
  m_page->drawLine(m_pageLeftMarginX + x, y1, m_pageLeftMarginX + x, y1 + totalHeight);
  x = 0.0;
}


static std::vector<std::string> split_keep_delim(const std::string &s, char delim, bool splitBefore=true)
{
	std::vector<std::string> elems;
  std::string cur;
  for (char c : s)
  {
    if (splitBefore && c == delim && !cur.empty())
    {
      elems.push_back(cur);
      cur = "";
    }
    cur += c;
    if (!splitBefore && c == delim)
    {
      elems.push_back(cur);
      cur = "";
    }
  }
  if (!cur.empty())
    elems.push_back(cur);
	return elems;
}


float DocVisitor::OutputTextHelper(const char* str, bool doSplit)
{
  std::string sstr(str);
  const auto p1 = sstr.find('[');
  const auto p2 = sstr.find(']');
  if (doSplit && p1 != std::string::npos && p2 != std::string::npos)
  {
    for (const auto& str2 : split_keep_delim(str, '['))
    {
      if ((x + 50) >= m_pageRightMarginX)
      {
        NewLine();
      }
    
      for (const auto& str3 : split_keep_delim(str2, ']', false))
      {
        // x += OutputTextHelper(("XX" + str3 + "YY").c_str(), false);
        x += OutputTextHelper(str3.c_str(), false);
      }
    }
    return 0.0;
  }

  if (sstr == "<center>")
  {
    alignCenter = true;
    return 0;
  }
  else if (sstr == "</center>")
  {
    alignCenter = false;
    return 0;
  }

  // Custom page-break markdown  (TODO: probably shouldn't be parsing markdown in here)
  //   - yep, if output html, we would want that filtered out from the html, so where to parse this?
  if (sstr == "<<<<<>>>>>" || sstr == "--<<>>--")
  {
    // Start a new page
    AddPage();
    y = 0.0f;
    x = 0.0f;
    return 0;
  }
  // TODO: be able to insert [AUTHOR], [TITLE], [VERSION], [COMPANY] and [COPYRIGHT] - can do, but it is [%TITLE] etc
  //       and also for the below, probably need to filter out from when doing html output, so probably this is
  //       not the right place to make these substitutions
  else if (sstr == "[PAGENUM]")
  {
    std::string num = std::to_string(m_pageNum);
    return OutputTextHelper(num.c_str());
  }
  else if (sstr == "[PAGECOUNT]")
  {
    if (m_pageCount > 0)
    {
      std::string num = std::to_string(m_pageCount);
      return OutputTextHelper(num.c_str());
    }
    else
    {
      // needs to output/parse the PDF twice, once to determine the page count and second time to insert it
      m_needPageCount = true;
      return OutputTextHelper("-1");
    }
  }
  else if (sstr == "[DATE]")
  {
    std::string datestr = get_date_string("%A, %e %B %Y");
    return OutputTextHelper(datestr.c_str());
  }
  else if (sstr == "[TIME]")
  {
    std::string datestr = get_date_string("%X");
    return OutputTextHelper(datestr.c_str());
  }
  else if (sstr == "[TIMESTAMP]")
  {
    std::string datestr = get_date_string("%F %X");
    return OutputTextHelper(datestr.c_str());
  }

  float w = m_page->textWidth(str);

  /*
     if (insideCode)// || insideLink)
     {
     m_page->drawRect(m_pageLeftMarginX + x - 1, y + m_pageTopMarginY + 2, w + 2, fontSizes[currentHeadingLevel.top()] + 2);

     if (insideCode)
     m_page->setColor(0.1f, 0.1f, 0.1f); // dark green
     else
     m_page->setColor(0.3f, 0.3f, 1.0f); // blue
     }
     */

  /*
  if (insideCode)
  {
    // Output a grey-background to the code/preformatted section
    float fontSize = fontSizes[currentHeadingLevel.top()];
    m_page->setPenWidth(0);
    m_page->setFillColor(0xB0F0B0); // light green
    m_page->drawRect(m_pageLeftMarginX + x, y + m_pageTopMarginY - fontSize, w, fontSize * lineSpacing);
    
    // Output the code text
    m_page->setFontType(currentStyle.top());
    m_page->setFontSize(fontSize);
    m_page->setFillColor(0x208010); // dark green
  }
  */

  DrawText(m_pageLeftMarginX + x, y + m_pageTopMarginY, str);

  /*
  if (insideCode)
  {
    m_page->setFillColor(0); // black
  }
  */

  return w;
}


// outputs current word to pdf
// splits the string up by words if it has to, and outputs new words on a new line if it will over fill the current line
// but also if the word is too big for a complete line, then will split that word mid-word as needed.
void DocVisitor::OutputCurrentWord(const char* str)
{
  if (!str || !str[0])
    return;

  // Need to see if need to break mid-word if the word is too long for the line
  float wordWidthAndMargins = m_pageLeftMarginX + m_page->textWidth(str);

  if ((wordWidthAndMargins + x) >= m_pageRightMarginX) // Checking if it can fit on the current line or not
  {
    if (wordWidthAndMargins >= m_pageRightMarginX) // Can it fit on a new line by itself?
    {
      // we need to break up the text, write it char by char
      int i = 0;
      while (str[i]) {
        char s[2] = { str[i], 0 };
        // TODO: adding the dash is okay if the text is English, but not if text is other language, eg Japanese or Chinese
        float chWidthAndMargins = m_pageLeftMarginX + m_page->textWidth(s) + m_page->textWidth("-");
        if ((chWidthAndMargins + x) >= m_pageRightMarginX)
        {
          OutputTextHelper("-");
          NewLine();
        }
        x += OutputTextHelper(s);
        i++;
      }
      x += m_page->textWidth(" ");//OutputTextHelper(" ");
      /*
      float spaceWidth = m_page->textWidth(" ");
      if (insideCode)
      {
        // Output a grey-background to the code/preformatted section
        float fontSize = fontSizes[currentHeadingLevel.top()];
        m_page->setPenWidth(0);
        m_page->setFillColor(0xB0F0B0); // light green
        m_page->drawRect(m_pageLeftMarginX + x, y + m_pageTopMarginY - fontSize, spaceWidth, fontSize * lineSpacing);
      }
      x += spaceWidth;
      */
      return;
    }
    else
    {
      NewLine();
    }
  }
  x += OutputTextHelper(str);
  x += m_page->textWidth(" ");//OutputTextHelper(" ");//m_page->textWidth(" ");
}


void DocVisitor::OutputText(const char* str)
{
  if (insideTable)
  {
    tableData.back().back().str += str;
    return;
  }

  if (insideCode)
  {
    codeText += str;
    return;
  }

  m_page->setFontType(currentStyle.top());
  m_page->setFontSize(fontSizes[currentHeadingLevel.top()]);

  // if (startOfListItem)
  {
    if (strlen(str) >= 3 && str[0] == '[' && str[2] == ']')
    {
      bool checked = str[1] == 'x' || str[1] == 'X';
      float textHeight = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.75;
      m_page->drawCheckBox(checked, m_pageLeftMarginX + x, m_pageTopMarginY + y - textHeight, textHeight, textHeight);
      x += textHeight * 1.5;
      str += 3;
    }
    startOfListItem = false;
  }

  std::string currentWord;
  int i = 0;
  while ( str[i] != 0 ) {
    if ( str[i] == ' ' || str[i] == '\t' || str[i] == '\n' ) {
      OutputCurrentWord(currentWord.c_str());

      if (insideCode && str[i] == '\n') { // for html, this is wrong
        NewLine();
      }	

      currentWord = "";
      //break;
    }
    else
    {
      char s[2] = { str[i], 0 };
      currentWord += s;
    }
    i++;
  }
  //if ( str[i] == 0 ) {
  if ( !currentWord.empty() ) {
    OutputCurrentWord(currentWord.c_str());
  }
}


/// Visit a text node
bool DocVisitor::Visit( const TiXmlText& text )
{
  const char* str = text.ValueTStr().c_str();
  OutputText(str);
  return true;
}

