/******************************************************************************
*
* Copyright (C) 1997-2019 by Dimitri van Heesch.
*
* Permission to use, copy, modify, and distribute this software and its
* documentation under the terms of the GNU General Public License is hereby
* granted. No representations are made about the suitability of this software
* for any purpose. It is provided "as is" without express or implied warranty.
* See the GNU General Public License for more details.
*
* Documents produced by analyze are derivative works derived from the
* input used in their production; they are not affected by this license.
*
*/

#include <cassert>
#include <cmath>

#ifdef _MSC_VER
#pragma warning( push )
#pragma warning( disable : 4242 )
#pragma warning( disable : 4244 )
#pragma warning( disable : 4996 )
#pragma warning( disable : 4456 )
#pragma warning( disable : 4805 )
#endif
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-Wshadow"
#endif
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#endif
#include <gunzip.hh>
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
#ifdef _MSC_VER
#pragma warning( pop )
#endif

#include "dotrunner.h"
#include "util.h"
#include "portable.h"
#include "dot.h"
#include "message.h"
#include "config.h"
#include "dir.h"
#include "analyze.h"

// the graphicx LaTeX has a limitation of maximum size of 16384
// To be on the save side we take it a little bit smaller i.e. 150 inch * 72 dpi
// It is anyway hard to view these size of images
#define MAX_LATEX_GRAPH_INCH  150
#define MAX_LATEX_GRAPH_SIZE  (MAX_LATEX_GRAPH_INCH * 72)

//#define DBG(x) printf x
#define DBG(x) do {} while(0)

//-----------------------------------------------------------------------------------------

// since dot silently reproduces the input file when it does not
// support the PNG format, we need to check the result.
static void checkPngResult(const OGString &imgName)
{
  FILE *f = Portable::fopen(imgName,"rb");
  if (f)
  {
    char data[4];
    if (fread(data,1,4,f)==4)
    {
      if (!(data[1]=='P' && data[2]=='N' && data[3]=='G'))
      {
        err("Image '{}' produced by dot is not a valid PNG!\n"
          "You should either select a different format "
          "(DOT_IMAGE_FORMAT in the config file) or install a more "
          "recent version of graphviz (1.7+)\n",imgName
        );
      }
    }
    else
    {
      err("Could not read image '{}' generated by dot!\n",imgName);
    }
    fclose(f);
  }
  else
  {
    err("Could not open image '{}' generated by dot!\n",imgName);
  }
}

static bool resetPDFSize(const int width,const int height, const OGString &base)
{
  OGString tmpName   = base+".tmp";
  OGString patchFile = base+".dot";
  Dir thisDir;
  if (!thisDir.rename(patchFile.str(),tmpName.str()))
  {
    err("Failed to rename file {} to {}!\n",patchFile,tmpName);
    return FALSE;
  }
  std::ifstream fi = Portable::openInputStream(tmpName);
  std::ofstream t  = Portable::openOutputStream(patchFile);
  if (!fi.is_open())
  {
    err("problem opening file {} for patching!\n",tmpName);
    thisDir.rename(tmpName.str(),patchFile.str());
    return FALSE;
  }
  if (!t.is_open())
  {
    err("problem opening file {} for patching!\n",patchFile);
    thisDir.rename(tmpName.str(),patchFile.str());
    return FALSE;
  }
  std::string line;
  while (getline(fi,line)) // foreach line
  {
    if (line.find("LATEX_PDF_SIZE") != std::string::npos)
    {
      double scale = (width > height ? width : height)/double(MAX_LATEX_GRAPH_INCH);
      t << "  size=\""<<width/scale << "," <<height/scale << "\";\n";
    }
    else
      t << line << "\n";
  }
  fi.close();
  t.close();
  // remove temporary file
  thisDir.remove(tmpName.str());
  return TRUE;
}

bool DotRunner::readBoundingBox(const OGString &fileName,int *width,int *height,bool isEps)
{
  std::ifstream f = Portable::openInputStream(fileName);
  if (!f.is_open())
  {
    err("Failed to open file {} for extracting bounding box\n",fileName);
    return false;
  }

  // read file contents into string 'contents'
  std::stringstream buffer;
  buffer << f.rdbuf();
  std::string contents = buffer.str();

  // start of bounding box marker we are looking for
  const std::string boundingBox = isEps ? "%%PageBoundingBox:" : "/MediaBox [";

  // helper routine to extract the bounding boxes width and height
  auto extractBoundingBox = [&fileName,&boundingBox,&width,&height](const char *s) -> bool
  {
    int x=0, y=0;
    double w=0, h=0;
    if (sscanf(s+boundingBox.length(),"%d %d %lf %lf",&x,&y,&w,&h)==4)
    {
      *width  = static_cast<int>(std::ceil(w));
      *height = static_cast<int>(std::ceil(h));
      return true;
    }
    err("Failed to extract bounding box from generated diagram file {}\n",fileName);
    return false;
  };

  // compressed segment start and end markers
  const std::string streamStart = "stream\n";
  const std::string streamEnd = "\nendstream";

  auto detectDeflateStreamStart = [&streamStart](const char *s)
  {
    size_t len = streamStart.length();
    bool streamOK = strncmp(s,streamStart.c_str(),len)==0;
    if (streamOK) // ASCII marker matches, check stream header bytes as well
    {
      unsigned short header1 = static_cast<unsigned char>(s[len])<<8; // CMF byte
      if (header1) // not end of string
      {
        unsigned short header = (static_cast<unsigned char>(s[len+1])) | header1; // FLG byte
        // check for correct header (see https://www.rfc-editor.org/rfc/rfc1950)
        return ((header&0x8F20)==0x0800) && (header%31)==0;
      }
    }
    return false;
  };

  const size_t l = contents.length();
  size_t i=0;
  while (i<l)
  {
    if (!isEps && contents[i]=='s' && detectDeflateStreamStart(&contents[i]))
    { // compressed stream start
      int col=17;
      i+=streamStart.length();
      const size_t start=i;
      DBG(("---- start stream at offset %08x\n",(int)i));
      while (i<l)
      {
        if (contents[i]=='\n' && strncmp(&contents[i],streamEnd.c_str(),streamEnd.length())==0)
        { // compressed block found in range [start..i]
          DBG(("\n---- end stream at offset %08x\n",(int)i));
          // decompress it into decompressBuf
          std::vector<char> decompressBuf;
          const char *source = &contents[start];
          const size_t sourceLen = i-start;
          size_t sourcePos = 0;
          decompressBuf.reserve(sourceLen*2);
          auto getter = [source,&sourcePos,sourceLen]() -> int {
            return sourcePos<sourceLen ? static_cast<unsigned char>(source[sourcePos++]) : EOF;
          };
          auto putter = [&decompressBuf](const char c) -> int {
            decompressBuf.push_back(c); return c;
          };
          Deflate(getter,putter);
          // convert decompression buffer to string
          std::string s(decompressBuf.begin(), decompressBuf.end());
          DBG(("decompressed_data=[[[\n%s\n]]]\n",s.c_str()));
          // search for bounding box marker
          const size_t idx = s.find(boundingBox);
          if (idx!=std::string::npos) // found bounding box in uncompressed data
          {
            return extractBoundingBox(s.c_str()+idx);
          }
          // continue searching after end stream marker
          i+=streamEnd.length();
          break;
        }
        else // compressed stream character
        {
          if (col>16) { col=0; DBG(("\n%08x: ",static_cast<int>(i))); }
          DBG(("%02x ",static_cast<unsigned char>(contents[i])));
          col++;
          i++;
        }
      }
    }
    else if (((isEps && contents[i]=='%') || (!isEps && contents[i]=='/')) &&
             strncmp(&contents[i],boundingBox.c_str(),boundingBox.length())==0)
    { // uncompressed bounding box
      return extractBoundingBox(&contents[i]);
    }
    else // uncompressed stream character
    {
      i++;
    }
  }
  err("Failed to find bounding box in generated diagram file {}\n",fileName);
  // nothing found
  return false;
}

//---------------------------------------------------------------------------------

DotRunner::DotRunner(const OGString& absDotName, const OGString& md5Hash)
  : m_file(absDotName)
  , m_md5Hash(md5Hash)
  , m_dotExe(analyze::verifiedDotPath)
  , m_cleanUp(Config_getBool(DOT_CLEANUP))
{
}


void DotRunner::addJob(const OGString &format, const OGString &output,
                      const OGString &srcFile,int srcLine)
{

  for (auto& s: m_jobs)
  {
    if (s.format != format) continue;
    if (s.output != output) continue;
    // we have this job already
    return;
  }
  auto args = OGString("-T") + format + " -o \"" + output + "\"";
  m_jobs.emplace_back(format, output, args, srcFile, srcLine);
}

OGString getBaseNameOfOutput(const OGString &output)
{
  int index = output.findRev('.');
  if (index < 0) return output;
  return output.left(index);
}

bool DotRunner::run()
{
  int exitCode=0;

  OGString dotArgs;

  OGString srcFile;
  int srcLine=-1;

  // create output
  if (Config_getBool(DOT_MULTI_TARGETS))
  {
    dotArgs=OGString("\"")+m_file+"\"";
    for (auto& s: m_jobs)
    {
      dotArgs+=' ';
      dotArgs+=s.args;
    }
    if (!m_jobs.empty())
    {
      srcFile = m_jobs.front().srcFile;
      srcLine = m_jobs.front().srcLine;
    }
    if ((exitCode=Portable::system(m_dotExe,dotArgs,FALSE))!=0) goto error;
  }
  else
  {
    for (auto& s : m_jobs)
    {
      srcFile = s.srcFile;
      srcLine = s.srcLine;
      dotArgs=OGString("\"")+m_file+"\" "+s.args;
      if ((exitCode=Portable::system(m_dotExe,dotArgs,FALSE))!=0) goto error;
    }
  }

  // check output
  // As there should be only one pdf file be generated, we don't need code for regenerating multiple pdf files in one call
  for (auto& s : m_jobs)
  {
    if (s.format.startsWith("pdf"))
    {
      int width=0,height=0;
      if (!readBoundingBox(s.output,&width,&height,FALSE)) goto error;
      if ((width > MAX_LATEX_GRAPH_SIZE) || (height > MAX_LATEX_GRAPH_SIZE))
      {
        if (!resetPDFSize(width,height,getBaseNameOfOutput(s.output))) goto error;
        dotArgs=OGString("\"")+m_file+"\" "+s.args;
        if ((exitCode=Portable::system(m_dotExe,dotArgs,FALSE))!=0) goto error;
      }
    }

    if (s.format.startsWith("png"))
    {
      checkPngResult(s.output);
    }
  }

  // remove .dot files
  if (m_cleanUp)
  {
    //printf("removing dot file %s\n",OPrint(m_file));
    Portable::unlink(m_file);
  }

  // create checksum file
  if (!m_md5Hash.isEmpty())
  {
    OGString md5Name = getBaseNameOfOutput(m_file) + ".md5";
    FILE *f = Portable::fopen(md5Name,"w");
    if (f)
    {
      fwrite(m_md5Hash.data(),1,32,f);
      fclose(f);
    }
  }
  return TRUE;
error:
  err_full(srcFile,srcLine,"Problems running dot: exit code={}, command='{}', arguments='{}'",
    exitCode,m_dotExe,dotArgs);
  return FALSE;
}


