#include "pkgutil.h"
#include <iostream>
#include <fstream>
#include <iterator>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <csignal>
#include <ext/stdio_filebuf.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <fcntl.h>
#include <zlib.h>
#include <libtar.h>

using namespace __gnu_cxx;

static int gzopen_frontend(char *pathname, int oflags, int mode)
{
   char* gzoflags;
   
   switch (oflags & O_ACCMODE) {
   case O_WRONLY:
      gzoflags = "w";
      break;

   case O_RDONLY:
      gzoflags = "r";
      break;

   case O_RDWR:
   default:
      errno = EINVAL;
      return -1;
   }

   int fd;
   gzFile gzf;

   if ((fd = open(pathname, oflags, mode)) == -1)
      return -1;
   
   if ((oflags & O_CREAT) && fchmod(fd, mode))
      return -1;
   
   if (!(gzf = gzdopen(fd, gzoflags))) {
      errno = ENOMEM;
      return -1;
   }
   
   return (int)gzf;
}

static tartype_t gztype = {
   (openfunc_t)gzopen_frontend,
   (closefunc_t)gzclose,
   (readfunc_t)gzread,
   (writefunc_t)gzwrite
};

pkgutil::pkgutil()
{
   // Ignore signals
   struct sigaction sa;
   memset(&sa, 0, sizeof(sa));
   sa.sa_handler = SIG_IGN;
   sigaction(SIGHUP, &sa, 0);
   sigaction(SIGINT, &sa, 0);
   sigaction(SIGQUIT, &sa, 0);
   sigaction(SIGTERM, &sa, 0);
}

void pkgutil::db_open(const string& path)
{
   root = trim_filename(path + "/");
   const string filename = root + PKG_DB;

   ifstream in(filename.c_str());
   if (in) {
      while (!in.eof()) {
         // Read record
         string name;
         pkginfo_t info;
         getline(in, name);
         getline(in, info.version);
         for (;;) {
            string file;
            getline(in, file);
            
            if (file.empty())
               break; // End of record
            
            info.files.insert(info.files.end(), file);
         }
         if (!info.files.empty())
            packages[name] = info;
      }
      in.close();
   } else {
      const char* msg = strerror(errno);
      print_error() << "unable to read " << filename << ": " << msg << endl;
      exit(EXIT_ERROR);
   }

#ifndef NDEBUG
   cerr << packages.size() << " packages found in database" << endl;
#endif
}

void pkgutil::db_commit()
{
   const string dbfilename = root + PKG_DB;
   const string dbfilename_new = dbfilename + ".incomplete_transaction";
   const string dbfilename_bak = dbfilename + ".backup";

   // Remove failed transaction (if it exists)
   if (unlink(dbfilename_new.c_str()) == -1 && errno != ENOENT) {
      const char* msg = strerror(errno);
      print_error() << "unable to remove " << dbfilename_new << ": " << msg << endl;
      exit(EXIT_ERROR);
   }

   // Write new database
   int fd_new = creat(dbfilename_new.c_str(), 0444);
   if (fd_new != -1) {
      stdio_filebuf<char> filebuf_new(fd_new, ios::out, true, getpagesize());
      ostream db_new(&filebuf_new);
      for (packages_t::const_iterator i = packages.begin(); i != packages.end(); i++) {
         if (!(*i).second.files.empty()) {
            db_new << (*i).first << "\n";
            db_new << (*i).second.version << "\n";
            copy((*i).second.files.begin(), (*i).second.files.end(), ostream_iterator<string>(db_new, "\n"));
            db_new << "\n";
         }
      }
      db_new.flush();
      fsync(fd_new);

      // Make backup of previous database
      int fd_bak = creat(dbfilename_bak.c_str(), 0444);
      if (fd_bak != -1) {
	 stdio_filebuf<char> filebuf_bak(fd_bak, ios::out, true, getpagesize());
	 ostream db_bak(&filebuf_bak);
         ifstream db(dbfilename.c_str());
         if (db) {
            db_bak << db.rdbuf();
            db.close();
         } else {
            const char* msg = strerror(errno);
            print_error() << "unable read " << dbfilename << ": " << msg << endl;
            exit(EXIT_ERROR);
         }
	 db_bak.flush();
         fsync(fd_bak);
      } else {
         const char* msg = strerror(errno);
         print_error() << "unable write " << dbfilename_bak << ": " << msg << endl;
         exit(EXIT_ERROR);
      }

      // Rename new database
      if (rename(dbfilename_new.c_str(), dbfilename.c_str()) == -1) {
         const char* msg = strerror(errno);
         print_error() << "unable rename " << dbfilename_new << " to " << dbfilename << ": " << msg << endl;
         exit(EXIT_ERROR);
      }
   } else {
      const char* msg = strerror(errno);
      print_error() << "unable to write " << dbfilename << ": " << msg << endl;
      exit(EXIT_ERROR);
   }

#ifndef NDEBUG
   cerr << packages.size() << " packages written to database" << endl;
#endif
}

void pkgutil::db_add_pkg(const string& name, const pkginfo_t& info)
{
   packages[name] = info;
}

bool pkgutil::db_find_pkg(const string& name)
{
   return (packages.find(name) != packages.end());
}

void pkgutil::db_rm_pkg(const string& name)
{
   set<string> files = packages[name].files;
   packages.erase(name);

#ifndef NDEBUG
   cerr << "Removing package phase 1 (all files in package):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Don't delete files that still have references
   for (packages_t::const_iterator i = packages.begin(); i != packages.end(); i++)
      for (set<string>::const_iterator j = (*i).second.files.begin(); j != (*i).second.files.end(); j++)
         files.erase((*j));

#ifndef NDEBUG
   cerr << "Removing package phase 2 (files that still have references excluded):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Delete the files
   for (set<string>::const_reverse_iterator i = files.rbegin(); i != files.rend(); i++) {
      const string filename = root + (*i);
      if (file_exists(filename) && remove(filename.c_str()) == -1) {
         const char* msg = strerror(errno);
         print_error() << "unable to remove " << filename << ": " << msg << endl;
      }
   }
}

void pkgutil::db_rm_pkg(const string& name, const set<string>& keep_list)
{
   set<string> files = packages[name].files;
   packages.erase(name);

#ifndef NDEBUG
   cerr << "Removing package phase 1 (all files in package):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Don't delete files found in the keep list
   for (set<string>::const_iterator i = keep_list.begin(); i != keep_list.end(); i++)
      files.erase(*i);

#ifndef NDEBUG
   cerr << "Removing package phase 2 (files that is in the keep list excluded):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Don't delete files that still have references
   for (packages_t::const_iterator i = packages.begin(); i != packages.end(); i++)
      for (set<string>::const_iterator j = (*i).second.files.begin(); j != (*i).second.files.end(); j++)
         files.erase((*j));

#ifndef NDEBUG
   cerr << "Removing package phase 3 (files that still have references excluded):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Delete the files
   for (set<string>::const_reverse_iterator i = files.rbegin(); i != files.rend(); i++) {
      const string filename = root + (*i);
      if (file_exists(filename) && remove(filename.c_str()) == -1) {
         if (errno == ENOTEMPTY)
            continue;
         const char* msg = strerror(errno);
         print_error() << "unable to remove " << filename << ": " << msg << endl;
      }
   }
}

void pkgutil::db_rm_files(set<string> files)
{
   // Remove all references
   for (packages_t::iterator i = packages.begin(); i != packages.end(); i++)
      for (set<string>::const_iterator j = files.begin(); j != files.end(); j++)
         (*i).second.files.erase((*j));
   
#ifndef NDEBUG
   cerr << "Removing files:" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Delete the files
   for (set<string>::const_reverse_iterator i = files.rbegin(); i != files.rend(); i++) {
      const string filename = root + (*i);
      if (file_exists(filename) && remove(filename.c_str()) == -1) {
         if (errno == ENOTEMPTY)
            continue;
         const char* msg = strerror(errno);
         print_error() << "unable to remove " << filename << ": " << msg << endl;
      }
   }
}

set<string> pkgutil::db_find_conflicts(const string& name, const pkginfo_t& info)
{
   set<string> files;
   
   // Find conflicting files in database
   for (packages_t::const_iterator i = packages.begin(); i != packages.end(); i++) {
      if ((*i).first != name) {
         set_intersection(info.files.begin(), info.files.end(),
                          (*i).second.files.begin(), (*i).second.files.end(),
                          inserter(files, files.end()));
      }
   }
   
#ifndef NDEBUG
   cerr << "Conflicts phase 1 (conflicts in database):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Find conflicting files in filesystem
   for (set<string>::iterator i = info.files.begin(); i != info.files.end(); i++) {
      const string filename = root + (*i);
      if (file_exists(filename) && files.find(*i) == files.end())
         files.insert(files.end(), *i);
   }

#ifndef NDEBUG
   cerr << "Conflicts phase 2 (conflicts in filesystem added):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // Exclude directories
   set<string> tmp = files;
   for (set<string>::const_iterator i = tmp.begin(); i != tmp.end(); i++) {
      if ((*i)[(*i).length() - 1] == '/')
         files.erase(*i);
   }

#ifndef NDEBUG
   cerr << "Conflicts phase 3 (directories excluded):" << endl;
   copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
   cerr << endl;
#endif

   // If this is an upgrade, remove files already owned by this package
   if (packages.find(name) != packages.end()) {
      for (set<string>::const_iterator i = packages[name].files.begin(); i != packages[name].files.end(); i++)
         files.erase(*i);

#ifndef NDEBUG
      cerr << "Conflicts phase 4 (files already owned by this package excluded):" << endl;
      copy(files.begin(), files.end(), ostream_iterator<string>(cerr, "\n"));
      cerr << endl;
#endif
   }

   return files;
}

pair<string, pkgutil::pkginfo_t> pkgutil::pkg_open(const string& filename) const
{
   pair<string, pkginfo_t> result;
   unsigned int i;
   TAR* t;

   // Extract name and version from filename
   string basename(filename, filename.rfind('/') + 1);
   string name(basename, 0, basename.find(VERSION_DELIM));
   string version(basename, 0, basename.rfind(PKG_EXT));
   version.erase(0, version.find(VERSION_DELIM)==string::npos?string::npos:version.find(VERSION_DELIM) + 1);
   
   if (name.empty() || version.empty()) {
      print_error() << "unable to determine name and/or version of " << basename << ": Invalid package name" << endl;
      exit(EXIT_ERROR);
   }

   result.first = name;
   result.second.version = version;

   if (tar_open(&t, const_cast<char*>(filename.c_str()), &gztype, O_RDONLY, 0, TAR_GNU) == -1) {
      const char* msg = strerror(errno);
      print_error() << "unable to open " << filename << ": " << msg << endl;
      exit(EXIT_ERROR);
   }

   for (i = 0; !th_read(t); i++) {
      result.second.files.insert(result.second.files.end(), th_get_pathname(t));
      if (TH_ISREG(t) && tar_skip_regfile(t)) {
         const char* msg = strerror(errno);
         print_error() << "unable to read " << filename << ": " << msg << endl;
         exit(EXIT_ERROR);
      }
   }
   
   if (i == 0) {
      const char* msg = strerror(errno);
      print_error() << "unable to read " << filename << ": " << msg << endl;
      exit(EXIT_ERROR);
   }

   tar_close(t);

   return result;
}

void pkgutil::pkg_install(const string& filename) const
{
   TAR* t;

   if (tar_open(&t, const_cast<char*>(filename.c_str()), &gztype, O_RDONLY, 0, TAR_GNU) == -1) {
      const char* msg = strerror(errno);
      print_error() << "unable to open " << filename << ": " << msg << endl;
      exit(EXIT_ERROR);
   }

   while (!th_read(t)) {
      const string archive_filename = th_get_pathname(t);
      string real_filename = trim_filename(root + string("/") + archive_filename);

      if (file_exists(real_filename) && real_filename[real_filename.length() - 1] != '/') {
         real_filename = trim_filename(root + string("/") + string(PKG_REJECTED) + archive_filename);
         print_info() << "rejecting " << archive_filename << ", keeping existing version" << endl;
      }

      if (tar_extract_file(t, const_cast<char*>(real_filename.c_str())) != 0) {
         const char* msg = strerror(errno);
         print_error() << "unable to read " << filename << ": " << msg << endl;
         exit(EXIT_ERROR);
      }
   }

   tar_close(t);
}

string pkgutil::trim_filename(const string& filename) const
{
   string search("//");
   string result = filename;

   for (string::size_type pos = result.find(search); pos != string::npos; pos = result.find(search)) {
      result.replace(pos, search.size(), "/");
   }

   return result;
}

bool pkgutil::file_exists(const string& filename) const
{
   struct stat buf;

   if (!lstat(filename.c_str(), &buf))
      return true;
   else
      return false;
}

void pkgutil::print_version() const
{
   cout << name() << " (rltools) " << VERSION << endl;
   exit(EXIT_OK);
}

void pkgutil::print_try_help() const
{
   cout << "Try '" << name() << " --help' for more information." << endl;
   exit(EXIT_ERROR);
}

void pkgutil::print_invalid_option(const string& option) const
{
   print_error() << "invalid option " << option << endl;
   print_try_help();
}

void pkgutil::print_option_missing() const
{
   print_error() << "option missing" << endl;
   print_try_help();
}

void pkgutil::check_argument(char** argv, int argc, int index) const
{
   if (argc - 1 < index + 1) {
      print_error() << "option " << argv[index] << " requires an argument" << endl;
      print_try_help();
   }
}