#include "stdafx.h"
#include "addon_installer.h"
#include <fstream>


Addon_installer::Addon_installer()
:
_newly_installed_count(0)
{
}


Addon_installer::~Addon_installer()
{
}


void Addon_installer::do_ensure_valid_state(std::string &err)
{
  Addon_proc::do_ensure_valid_state(err);

  if (!err.empty())
    return;
  else if (!_installed_addons)
    err= "Installed addons were not set.";
  else if (_guid.empty())
    err= "GUID was not set.";
}


void Addon_installer::do_execute()
{
  _newly_installed_count= 0;
  install(_guid, _min_ver, _max_ver);
}


void Addon_installer::install(const std::string &guid, const std::string &min_ver, const std::string &max_ver)
{
  Addon *addon;

  // check if needed addon is already installed
  if ((addon= _installed_addons->find(guid, min_ver, max_ver)))
    return;

  // check if conflicting version of addon is installed
  if ((addon= _installed_addons->find(guid, "", "")))
    _log->throw_err("Found previously installed " + addon->full_name() +
      ". Its version conflicts with one to be installed. Existing version must be uninstalled first.");

  // find addon in cache. by this moment it must be already downloaded.
  if (!(addon= _cached_addons->find(guid, min_ver, max_ver)))
    _log->throw_err("Failed to find required addon in cache: " + Addon::range_version_name(guid, min_ver, max_ver) + ".");

  // first install recursively all dependences
  for (Addon::Dependencies::const_iterator i= addon->dependencies.begin(), i_end= addon->dependencies.end(); i != i_end; ++i)
    install(i->guid, i->min_ver, i->max_ver);

  _log->push_info(addon->full_name());

  // extract files from package
  File_dir_list created_folders;
  File_list created_files;
  try
  {
    int err= 0;
    File package_file= _cached_addons->storage_dir().path() + "/" + addon->package_filename();
    Auto_handler<zip, int> zip_(&zip_close);
    zip_= zip_open(package_file.path().c_str(), 0, &err);
    if (!zip_)
    {
      std::string err_msg;
      switch (err)
      {
      case ZIP_ER_NOZIP:
        err_msg= "The file is not a addon package"; break;
      case ZIP_ER_MEMORY:
        err_msg= "Cannot allocate enough memory to open package"; break;
      case ZIP_ER_NOENT:
        err_msg= "File not found"; break;
      default:
        err_msg= "Cannot open file"; break;
      }
      _log->throw_err(err_msg + ": " + package_file.quoted_path() + ".");
    }
    else
    {
      extract(zip_, created_folders, created_files);

      File install_log_file= _installed_addons->storage_dir().path() + "/log/" + addon->install_log_filename();
      install_log_file.ensure_parent_dir_exists();

      // flush created folders/files to install log file
      {
        std::ofstream ofs(install_log_file.path().c_str());
        if (!ofs.is_open())
          _log->throw_err("Failed to open file for writing: " + install_log_file.quoted_path() + ".");
        std::ostream_iterator<std::string> i(ofs, "\n");
        std::copy(created_folders.begin(), created_folders.end(), i);
        std::copy(created_files.begin(), created_files.end(), i);
      }

      // copy manifest file to installed addons dir
      {
        File cached_manifest_file= _cached_addons->storage_dir().path() + "/" + addon->manifest_filename();
        File installed_manifest_file= _installed_addons->storage_dir().path() + "/" + addon->manifest_filename();
        if (!File::copy(cached_manifest_file, installed_manifest_file))
          _log->throw_err("Failed to copy file from " + cached_manifest_file.quoted_path() +
            " to " + installed_manifest_file.quoted_path() + ".");
      }

      _installed_addons->add(addon);
      ++_newly_installed_count;
    }
  }
  catch(std::exception &e)
  {
    _log->push_err(e.what());
    rollback(created_folders, created_files);
    _log->throw_err();
  }
}


void Addon_installer::rollback(File_dir_list &created_folders, File_list &created_files)
{
  _log->push_info("Rolling back installed files...");
  for (File_list::iterator i= created_files.begin(), i_end= created_files.end(); i != i_end; ++i)
    if (i->valid())
      (void)i->remove(); // TODO: make the list of files that failed to remove
  for (File_dir_list::iterator i= created_folders.begin(), i_end= created_folders.end(); i != i_end; ++i)
    if (i->valid())
      (void)i->remove(false); // TODO: make the list of folders that failed to remove
  _log->push_info("Rollback complete.");
}


void Addon_installer::extract(zip *zip_, File_dir_list &created_folders, File_list &created_files)
{
  //! check if zip_file is released on thrown exception without being allocated within try block
  
  // init path translation map
  typedef std::map<std::string, std::string> Path_transl_map;
  Path_transl_map path_transl_map;
  path_transl_map["base"]= _base_dir;
  path_transl_map["data"]= _data_dir;
  path_transl_map["user"]= _user_data_dir;

  int count= zip_get_num_files(zip_);
  for (int i= 0; i < count; ++i)
  {
    const char *fn= zip_get_name(zip_, i, 0);
    if (!fn)
      _log->throw_err("Failed to unpack package.");

    File file;
    bool is_folder;
    {
      std::string path= fn;
      is_folder= ('/' == *path.rbegin());
      std::string::size_type offset= path.find_first_of("/");
      std::string path_alias= path.substr(0, offset);
      Path_transl_map::const_iterator i= path_transl_map.find(path_alias);
      if (path_transl_map.end() == i) // top level parent dir doesn't correspond to any known folder alias
        continue;
      path= i->second + "/" + path.substr(offset);
      file.path(path);
    }

    if (!is_folder && file.exists())
      _log->throw_err("File already exists: " + file.quoted_path() +
        ".\nAddon owning this file must be uninstalled first or if it doesn't relate to any installed addon it must be deleted manually.");

    // remember all folders/files to be created
    {
      File_dir dir;
      if (is_folder)
        dir.path(file);
      else
        dir= file.parent_dir();

      while (dir.valid() && !dir.exists())
      {
        created_folders.push_back(dir);
        dir= dir.parent_dir();        
      }
    }

    if (is_folder)
    {
      File_dir dir(file.path());
      if (!dir.make_with_parents())
        _log->throw_err("Failed to create file directory: " + dir.quoted_path());
    }
    else
    {
      file.ensure_parent_dir_exists();

      Auto_handler<zip_file, int> zf(&zip_fclose);
      zf= zip_fopen_index(zip_, i, 0);

      if (!zf)
      {
        int ziperr, syserr;

        zip_error_get(zip_, &ziperr, &syserr);
        if (0 != ziperr)
        {
          if (ZIP_ER_NOENT == ziperr)
            _log->throw_err("File " + File(fn).quoted_path() + " was not found in the archive.");
        }
        char buffer[1000];
        zip_error_to_str(buffer, sizeof(buffer), ziperr, syserr);

        _log->throw_err(buffer);
      }

      if (!file.open("wb"))
        _log->throw_err("Failed to open file for writing: " + file.quoted_path() + ".");

      created_files.push_back(file);

      // extract single file
      {
        ssize_t bc;
        const size_t BUFFER_SIZE= 4096;
        char buffer[BUFFER_SIZE];
        FILE *fs= file.stream();
        while ((bc= zip_fread(zf, buffer, BUFFER_SIZE)))
        {
          if (bc != fwrite(buffer, 1, bc, fs))
            _log->throw_err("Failed to write to file: " + file.quoted_path() + ".");
        }
      }
    }
  }
}
