#include <algorithm>
#include "stdafx.h"

#include <zip.h>
#include "wb_model_file.h"

#include <algorithm>
#include <set>
#include <stdexcept>
#include <glib/gstdio.h>

#include <grtpp.h>
#include <grtpp_util.h>

#include "myx_util_public_interface.h"

#include "grt/common.h"

#include "grts/structs.workbench.h"


#define MAIN_DOCUMENT_NAME "document.mwb.xml"

#define DOCUMENT_FORMAT "MySQL Workbench Model"
// version history:
// switched to 1.1.6 in 5.0.20
// switched to 1.2.0 in 5.1.0 

// switched from 1.2.0 to 1.3.0 in 5.1.7
#define DOCUMENT_VERSION "1.3.1"

#define ZIP_FILE_FORMAT "1.0"

#define IMAGES_DIR "@images"
#define NOTES_DIR "@notes"
#define SCRIPTS_DIR "@scripts"

#define ZIP_FILE_COMMENT DOCUMENT_FORMAT" archive "ZIP_FILE_FORMAT


using namespace bec;
using namespace wb;


/*
static std::string strfmt_ziperror(zip *z, const char *msg)
{
  int ziperr, syserr;
  char buffer[1000];

  zip_error_get(z, &ziperr, &syserr);
  zip_error_to_str(buffer, sizeof(buffer), ziperr, syserr);

  return strfmt("%s: %s", msg, buffer);
}*/


void ModelFile::copy_file(const std::string &srcfile, const std::string &destfile)
{
  char buffer[4098];
  FILE *sf= myx_fopen(srcfile.c_str(), "rb");
  if (!sf)
    throw grt::os_error("Could not open file "+srcfile, errno);

  FILE *tf= myx_fopen(destfile.c_str(), "wb+");
  if (!tf)
  {
    fclose(sf);
    throw grt::os_error("Could not create file "+destfile, errno);
  }

  size_t c;
  while ((c= fread(buffer, 1, sizeof(buffer), sf)) > 0)
  {
    if (fwrite(buffer, 1, c, tf) < c)
    {
      int err= errno;
      fclose(sf);
      fclose(tf);
      throw grt::os_error("Error copying to file "+destfile, err);
    }
  }

  fclose(sf);
  fclose(tf);
}


static std::string create_document_dir(const std::string &dir, const std::string &prefix)
{
  std::string path;
  char s[10];
  int i= 0;

  strcpy(s, "d");
  for (;;)
  {
    path= dir+G_DIR_SEPARATOR_S+prefix+s;
    if (g_mkdir(path.c_str(), 0700) < 0)
    {
#ifdef _WIN32
      // errno is not reliable in windows
      if (!g_file_test(path.c_str(), G_FILE_TEST_EXISTS))
        throw grt::os_error("Cannot create directory for document.", errno);
#else
      if (errno != EEXIST)
        throw grt::os_error("Cannot create directory for document.", errno);
#endif
    }
    else
      break;
    sprintf(s, "d%i", ++i);
  }

  return path;
}


struct AutoLock
{
  GStaticRecMutex *mx;
  AutoLock(GStaticRecMutex &mutex) : mx(&mutex) { g_static_rec_mutex_lock(mx); }
  ~AutoLock() { g_static_rec_mutex_unlock(mx); }
};


ModelFile::ModelFile()
: _dirty(false)
{
  _temp_dir= g_get_tmp_dir();

  g_static_rec_mutex_init(&_mutex);
}


ModelFile::~ModelFile()
{
  cleanup();
  g_static_rec_mutex_free(&_mutex);
}


void ModelFile::copy_file_to(const std::string &file, const std::string &dest)
{
  copy_file(get_path_for(file), dest);
}


void ModelFile::open(const std::string &path)
{
  bool file_is_zip;

  AutoLock lock(_mutex);

  {
    FILE *f= myx_fopen(path.c_str(), "rb");
    if (!f)
      throw grt::os_error("Could not open file "+path+": "+strerror(errno));

    unsigned char buffer[10];
    size_t c;
    if ((c= fread(buffer, 1, 10, f)) < 10)
    {
      fclose(f);
      if (c == 0)
        throw std::runtime_error("File is empty.");
      else
        throw std::runtime_error("Invalid or corrupt file.");
    }
    fclose(f);

    if (buffer[0] == 0x50 && buffer[1] == 0x4b && buffer[2] == 0x03 && buffer[3] == 0x04 && buffer[4] == 0x14)
      file_is_zip= true;
    else
      file_is_zip= false;
  }

  gchar *basename= g_path_get_basename(path.c_str());
  _content_dir= create_document_dir(_temp_dir, basename);
  g_free(basename);

  if (file_is_zip)
  {
    unpack_zip(path, _content_dir);
  }
  else
  {
    std::string destpath= _content_dir;    
    destpath.append(G_DIR_SEPARATOR_S);
    destpath.append("document.mwb.xml");

    // assume old XML format and "convert" it
    copy_file(path, destpath);
  }

  _dirty= false;
}


void ModelFile::create()
{
  AutoLock lock(_mutex);

  _content_dir= create_document_dir(_temp_dir, "newmodel.mwb");
  _dirty= false;
}


std::string ModelFile::get_path_for(const std::string &file)
{
  return _content_dir+G_DIR_SEPARATOR_S+file;
}


// reading
workbench_DocumentRef ModelFile::retrieve_document(grt::GRT *grt)
{
  AutoLock lock(_mutex);

  xmlDocPtr xmldoc= grt->load_xml(get_path_for("document.mwb.xml"));

  try
  {
    workbench_DocumentRef doc(unserialize_document(grt, xmldoc, get_path_for("document.mwb.xml")));

    xmlFreeDoc(xmldoc);

    return doc;
  }
  catch (std::exception)
  {
    if (xmldoc)
      xmlFreeDoc(xmldoc);

    throw;
  }
}


void ModelFile::unpack_zip(const std::string &zipfile, const std::string &destdir)
{
  if (g_mkdir_with_parents(destdir.c_str(), 0700) < 0)
    throw grt::os_error(strfmt(_("Cannot create temporary directory for open document: %s"), destdir.c_str()), errno);

  int err;
  zip *z= zip_open(zipfile.c_str(), 0, &err);
  if (!z)
  {
    if (err == ZIP_ER_NOZIP)
      throw std::runtime_error("The file is not a Workbench document.");
    else if (err == ZIP_ER_MEMORY)
      throw grt::os_error("Cannot allocate enough memory to open document.");
    else if (err == ZIP_ER_NOENT)
      throw grt::os_error("File not found.");

    int len= zip_error_to_str(NULL, 0, 0, err);
    std::string msg;
    if (len > 0)
    {
      char *buf= (char*)g_malloc(len+1);
      zip_error_to_str(buf, len+1, 0, err);
      msg= buf;
      g_free(buf);
    }
    else
      msg= "error opening zip archive";

    zip_close(z);
    throw std::runtime_error(strfmt(_("Cannot open document file: %s"), msg.c_str()));
  }

  int count= zip_get_num_files(z);
  for (int i= 0; i < count; i++)
  {
    zip_file *file= zip_fopen_index(z, i, 0);
    if (!file)
    {
      const char *err= zip_strerror(z);
      zip_close(z);
      throw std::runtime_error(strfmt(_("Error opening document file: %s"), err));
    }

    const char *zname= zip_get_name(z, i, 0);
    if (strcmp(zname, "/") == 0 || strcmp(zname, "\\") == 0)
    {
      zip_fclose(file);
      continue;
    }
    gchar *dirname= g_path_get_dirname(zname);
    gchar *basename= g_path_get_basename(zname);

    std::string outpath= destdir;

    if (dirname && *dirname)
    {
      outpath.append(G_DIR_SEPARATOR_S);
      outpath.append(dirname);
      if (g_mkdir_with_parents(outpath.c_str(), 0700) < 0)
      {
        g_free(dirname);
        g_free(basename);
        zip_fclose(file);
        zip_close(z);
        throw grt::os_error(_("Error creating temporary directory while opending document."), errno);
      }
    }
    outpath.append(G_DIR_SEPARATOR_S);
    outpath.append(basename ? basename : "");
    g_free(dirname);
    g_free(basename);

    FILE *outfile= myx_fopen(outpath.c_str(), "wb+");
    if (!outfile)
    {
      zip_fclose(file);
      zip_close(z);
      throw grt::os_error(_("Error creating temporary file while opending document."), errno);
    }
    
    char buffer[4098];
    int c;
    while ((c= zip_fread(file, buffer, sizeof(buffer))) > 0)
    {
      if (fwrite(buffer, 1, c, outfile) < (size_t)c)
      {
        int err= ferror(outfile);
        fclose(outfile);
        zip_fclose(file);
        zip_close(z);
        throw grt::os_error(_("Error writing temporary file while opending document."), err);
      }
    }

    if (c < 0)
    {
      std::string err= zip_file_strerror(file) ? zip_file_strerror(file) : "";
      zip_fclose(file);
      zip_close(z);
      throw std::runtime_error(strfmt(_("Error opening document file: %s"), err.c_str()));
    }

    zip_fclose(file);
    fclose(outfile);
  }

  zip_close(z);
}


static void zip_dir_contents(zip *z, const std::string &destdir, const std::string &partial)
{
  GError *error= 0;
  GDir *dir= g_dir_open(destdir.empty() ? "." : destdir.c_str(), 0, &error);
  if (!dir)
  {
    zip_close(z);
    std::string err= error ? error->message : "Cannot open document directory.";
    g_error_free(error);
    throw grt::os_error(err);
  }

  // must add stuff in 2 steps, 1st files only and then dirs only
  for (int add_directories= 0; add_directories < 2; add_directories++)
  {
    const gchar *entry;
    while ((entry= g_dir_read_name(dir)))
    {
      std::string tmp= destdir;
      
      if (!tmp.empty())
        tmp.append(G_DIR_SEPARATOR_S).append(entry);
      else
        tmp.append(entry);
      
      if (g_file_test(tmp.c_str(), G_FILE_TEST_IS_DIR))
      {
        if (add_directories)
        {
          try
          {
            //not needed zip_add_dir(z, partial.c_str());
            zip_dir_contents(z, destdir.empty() ? entry : destdir+G_DIR_SEPARATOR+entry, tmp);
          }
          catch (...)
          {
            g_dir_close(dir);
            throw;
          }
        }
      }
      else
      {
        if (!add_directories)
        {
          zip_source *src= zip_source_file(z, tmp.c_str(), 0, 0);
          if (!src || zip_add(z, tmp.c_str(), src) < 0)
          {
            zip_source_free(src);
            g_dir_close(dir);
            throw std::runtime_error(zip_strerror(z));
          }
        }
      }
    }
    g_dir_rewind(dir);
  }
  g_dir_close(dir);  
}


void ModelFile::pack_zip(const std::string &zipfile, const std::string &destdir)
{
  int err= 0;
  std::string curdir;
  
  {
    gchar *cwd= g_get_current_dir();
    curdir= cwd;
    g_free(cwd);
  }
  
  
  // change to the document data directory (after opening the zip file)
  // so that the zip won't have the full paths of the contents stored
  if (g_chdir(destdir.c_str()) < 0)
    throw grt::os_error("chdir failed.");
  
  // zip_open will open an existing file even if ZIP_CREATE is specified, so
  // we have to 1st delete the file...
  zip *z= zip_open(zipfile.c_str(), ZIP_CREATE, &err);
  if (!z)
  {
    if (err == ZIP_ER_MEMORY)
      throw grt::os_error("Cannot allocate enough temporary memory to save document.");
    else if (err == ZIP_ER_NOENT)
      throw grt::os_error("File or directory not found.");
    else 
      throw grt::os_error("Cannot create file.");
  }

  zip_set_archive_comment(z, ZIP_FILE_COMMENT, sizeof(ZIP_FILE_COMMENT));

  try
  {
    zip_dir_contents(z, "", "");
    
    if (zip_close(z) < 0)
    {
      std::string err= zip_strerror(z) ? zip_strerror(z) : "";
      
      throw std::runtime_error(strfmt(_("Error writing zip file: %s"), err.c_str()));
    }
    
    g_chdir(curdir.c_str());
  }
  catch (...)
  {
    zip_close(z);
    g_chdir(curdir.c_str());
    throw;
  }
}




workbench_DocumentRef ModelFile::unserialize_document(grt::GRT *grt, xmlDocPtr xmldoc, const std::string &path)
{
  std::string doctype, version;

  grt->get_xml_metainfo(xmldoc, doctype, version);
  
  _loaded_version= version;

  // reset list of warnings found during load
  _load_warnings.clear();

  if (doctype != DOCUMENT_FORMAT)
    throw std::runtime_error("The file does not contain a Workbench document.");

  if (version != DOCUMENT_VERSION)
  {
    // first phase of document upgrade will upgrade it at XML level
    if (!attempt_xml_document_upgrade(xmldoc, version))
      throw std::runtime_error("The document was created in an incompatible version of the appplication.");
  }

  check_and_fix_inconsistencies(xmldoc, version);
  
  grt::ValueRef value(grt->unserialize_xml(xmldoc, path));

  if (!value.is_valid())
    throw std::runtime_error("Error unserializing document data.");

  if (!workbench_DocumentRef::can_wrap(value))
    throw std::runtime_error("Loaded file does not contain a valid Workbench document.");

  workbench_DocumentRef doc(workbench_DocumentRef::cast_from(value));
  
  // send phase will upgrade at GRT level
  doc= attempt_document_upgrade(doc, xmldoc, version);
  
  cleanup_upgrade_data();
  
  check_and_fix_inconsistencies(doc, version);

  return doc;
}


void ModelFile::save_to(const std::string &path)
{
  AutoLock lock(_mutex);

  if (g_file_test(path.c_str(), G_FILE_TEST_EXISTS))
  { 
    std::string tmp= path+".bak";
    g_remove(tmp.c_str());
    if (g_rename(path.c_str(), tmp.c_str()) < 0)
      throw grt::os_error("Could not backup existing file "+path, errno);
  }

  for (std::list<std::string>::const_iterator iter= _delete_queue.begin();
    iter != _delete_queue.end(); ++iter)
    g_remove(get_path_for(*iter).c_str());

  _delete_queue.clear();

  if (g_path_is_absolute(path.c_str()))
    pack_zip(path, _content_dir);
  else
  {
    char *prefix= g_get_current_dir();
    pack_zip(std::string(prefix).append("/").append(path), _content_dir);
    g_free(prefix);
  }

  _dirty= false;
}


static int rmdir_recursively(const char *path)
{
  int res= 0;
  GError *error= NULL;
  GDir* dir;
  const char *dir_entry;
  gchar *entry_path;
  
  dir= g_dir_open(path, 0, &error);
  if (!dir && error)
  {
    res= error->code;
    g_error_free(error);
    return res;
  }

  while ((dir_entry= g_dir_read_name(dir)))
  {
    entry_path= g_build_filename(path, dir_entry, NULL);
    if (g_file_test(entry_path, G_FILE_TEST_IS_DIR))
      (void) rmdir_recursively(entry_path);
    else
      (void) g_remove(entry_path);
    g_free(entry_path);
  }

  (void) g_rmdir(path);

  g_dir_close(dir);

  return res;
}


void ModelFile::cleanup()
{
  AutoLock lock(_mutex);

  rmdir_recursively(_content_dir.c_str());
}


/** Adds an external file to the document.
*/
static std::string add_attachment_file(const std::string &destdir, const std::string &path)
{
  gchar *base;
  std::string prefix= destdir + G_DIR_SEPARATOR_S;
  if (!path.empty())
  {
    base= g_path_get_basename(path.c_str());
    prefix += base;
    g_free(base);
  }

  int i= 1;
  std::string destfile= prefix;

  if (!g_file_test(destdir.c_str(), G_FILE_TEST_IS_DIR))
  {
    if (g_mkdir_with_parents(destdir.c_str(), 0700) < 0)
      throw grt::os_error("Could not create directory for attached file");
  }

  // if path is not supplied, default value of destfile would be filled with the dirname only
  if (path.empty())
    destfile= strfmt("%s%i", prefix.c_str(), i++);

  while (g_file_test(destfile.c_str(), G_FILE_TEST_EXISTS))
    destfile= strfmt("%s%i", prefix.c_str(), i++);

  if (path.empty())
  {
    FILE *f= myx_fopen(destfile.c_str(), "w+");
    if (f)
      fclose(f);
    else
      throw grt::os_error("Error creating attached file");
  }
  else
  {
    try
    {
      ModelFile::copy_file(path, destfile);
    }
    catch (std::exception &exc)
    {
      throw std::runtime_error(std::string("Error adding file to document: ").append(exc.what()));
    }
  }
  base= g_path_get_basename(destfile.c_str());
  destfile= base;
  g_free(base);

  base= g_path_get_basename(destdir.c_str());
  destfile= std::string(base).append("/").append(destfile);
  g_free(base);

  return destfile;
}


std::string ModelFile::add_image_file(const std::string &path)
{
  _dirty= true;

  return add_attachment_file(_content_dir+G_DIR_SEPARATOR_S+IMAGES_DIR, path);
}


std::string ModelFile::add_script_file(const std::string &path)
{
  _dirty= true;

  return add_attachment_file(_content_dir+G_DIR_SEPARATOR_S+SCRIPTS_DIR, path);
}


std::string ModelFile::add_note_file(const std::string &path)
{
  _dirty= true;

  return add_attachment_file(_content_dir+G_DIR_SEPARATOR_S+NOTES_DIR, path);
}


bool ModelFile::has_file(const std::string &name)
{
  AutoLock lock(_mutex);

  return g_file_test(get_path_for(name).c_str(), G_FILE_TEST_EXISTS) != 0;
}

void ModelFile::set_file_contents(const std::string &path, const std::string &data)
{
  set_file_contents(path, data.c_str(), data.size());
}

void ModelFile::set_file_contents(const std::string &path, const char *data, size_t size)
{
  std::string fpath= get_path_for(path);

  GError *error= NULL;
  g_file_set_contents(fpath.c_str(), data, size, &error);
  if (error != NULL)
    throw std::runtime_error(std::string("Error while setting file contents: ") + error->message);
}

std::string ModelFile::get_file_contents(const std::string &path)
{
  gchar *contents= 0;
  gsize length;
  std::string tmp;
  
  if (g_file_get_contents(get_path_for(path).c_str(), &contents, &length, NULL))
  {
    tmp= std::string(contents, length);
    g_free(contents);
    return tmp;
  }

  throw std::runtime_error("Error reading attached file contents.");
}


// writing
void ModelFile::store_document(grt::GRT *grt, const workbench_DocumentRef &doc)
{
  grt->serialize(doc, get_path_for("document.mwb.xml"), DOCUMENT_FORMAT, DOCUMENT_VERSION);

  _dirty= true;
}


void ModelFile::delete_file(const std::string &path)
{
  if (std::find(_delete_queue.begin(), _delete_queue.end(), path) == _delete_queue.end())
  {
    _dirty= true;
    _delete_queue.push_back(path);
  }
}


bool ModelFile::undelete_file(const std::string &path)
{
  std::list<std::string>::iterator iter;

  if ((iter= std::find(_delete_queue.begin(), _delete_queue.end(), path)) == _delete_queue.end())
    return false;

  _dirty= true;
  _delete_queue.erase(iter);

  return true;
}


cairo_surface_t *ModelFile::get_image(const std::string &path)
{
  cairo_surface_t *image;
  cairo_status_t st;

  image= cairo_image_surface_create_from_png(get_path_for(path).c_str());
  if (!image || (st= cairo_surface_status(image)) != CAIRO_STATUS_SUCCESS)
  {
    if (image)
      cairo_surface_destroy(image);
    return 0;
  }

  return image;
}

