/* fbsdInterface.cc
**
** Copyright (C) 2000 by Alex Hayward <xelah@xelah.com>
**
*/


/*
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program in a file called COPYING; if not, write to
** the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
** MA 02111-1307, USA.
*/

/*
** Bug reports and questions can be sent to kde-devel@kde.org
*/



#include <errno.h>
#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#include <qstringlist.h>

#include <klocale.h>
#include <kglobal.h>
#include <kiconloader.h>
#include <kdebug.h>

#include "fbsdInterface.h"
#include "kpackage.h"
#include "updateLoc.h"
#include "cache.h"
#include "options.h"

#define PKG_INFO_BIN "/usr/sbin/pkg_info"
#define PKG_ADD_BIN "/usr/sbin/pkg_add"
#define PKG_DELETE_BIN "/usr/sbin/pkg_delete"

fbsdInterface::fbsdInterface():pkgInterface() {
  head = "BSD";
  name = i18n("BSD");
  icon = "bsd";

  pict = UserIcon(icon);
  updated_pict = UserIcon("bupdated");
  new_pict = UserIcon("bnew");

  packagePattern = "*.tgz";
  typeID = "/tgz";

  queryMsg = i18n("Querying package list: ");

  locatedialog = new Locations(i18n("Location of BSD Packages and Ports"));
  locatedialog->dLocations(1, 1, this, i18n("Ports"), "Pkg", "*.tgz",
                           i18n("Location of Ports Tree (e.g. /usr/ports or /usr/opt)"),FALSE);
  locatedialog->dLocations(1, 6, this, i18n("Packages"), "Pkg", "*.tgz",
                           i18n("Location of Folders Containing BSD Packages or Package Trees"));
  connect(locatedialog, SIGNAL(returnVal(LcacheObj *)), this, SLOT(setAvail(LcacheObj *)));
  locatedialog->apply_slot();

  paramsInst.append(new param(i18n("Ignore Scripts"),FALSE,FALSE,"-I"));
  paramsInst.append(new param(i18n("Check Dependencies"),TRUE,TRUE,"-f"));
  paramsInst.append(new param(i18n("Test (do not install)"),FALSE,FALSE,"-n"));

 paramsUninst.append(new param(i18n("Ignore Scripts"),FALSE,FALSE, "-I"));
 paramsUninst.append(new  param(i18n("Check Dependencies"),TRUE,TRUE, "-f"));
 paramsUninst.append(new param(i18n("Test (do not uninstall)"),FALSE,FALSE, "-n"));


}

fbsdInterface::~fbsdInterface() {

}

bool fbsdInterface::isType(char *, const QString &fname) {
  // These files are .tgz files. Pass it to pkg_info and see whether it
  // succeeds.
  reader.setup(PKG_INFO_BIN);
  *reader.proc << fname;
  if (reader.start(0, false)) return true;
  return false;
}

static void insertGroups(QDict<QString> *a, const char *cats)
{
  /* Create the list of groups (which is space-separated), and then
  ** iterate through it with the iterator i. count is just to 
  ** distinguish the first entry (count==0) from the rest, since
  ** the key used in a->insert() needs to be different.
  */
  QStringList grlist = QStringList::split(' ',QString(cats));
  unsigned int count = 0;
  for (QStringList::Iterator i = grlist.begin(); 
    i != grlist.end(); ++count,++i) {
    a->insert( (count ? "also in" : "group"), new QString(*i));
  }
}

packageInfo *fbsdInterface::getPackageInfo(char mode, const QString &pname, const QString &version) {
  QString name( pname);
  bool installed = false;
  kpackage->setStatus(i18n("Getting package info"));

  kdDebug() << "Looking at package " << pname << endl;

  if (mode == 'i' && !version.isEmpty()) {
    name += "-" + version;
  }

  QDict<QString> *a = new QDict<QString>;
  a->setAutoDelete(TRUE);

  // Get the package name first (for mode = 'u').
  if (mode == 'u') {
    reader.setup(PKG_INFO_BIN);
    *reader.proc << "-qf" << name;

    int last_dir = name.find('/');
    if (last_dir != -1) {
      a->insert("filename", new QString(name.mid(last_dir+1)) );
      a->insert("base", new QString(name.left(last_dir + 1)) );
    } else {
      a->insert("filename", new QString(name));
      a->insert("base", new QString());
    }

    // Look for a line of the form '@name <pkgname>'
    if (reader.start(0, FALSE)) {
      if (!reader.buf.isNull()) {
	char *buf = strdup(reader.buf.ascii());
	char *n = strtok(buf, "\n");

	while (n != 0) {
	  if (!memcmp(n, "@name", sizeof("@name")-1)) {
	    n += sizeof("@name ")-1;
	    while (isspace(*n)) n++;
	    addNV(a, n);
	    break;
	  }
	  n = strtok(0, "\n");
	}
	free(buf);
      }
    }
  } else addNV(a, name);

  // Open a pipe to a pkg_info process in order to read the one line comment
  // and description for the package. This works for both installed packages
  // and for files.
  reader.setup(PKG_INFO_BIN);
  *reader.proc << "-q" << name;

  if (reader.start(0, false)) {
    char *buf = strdup(reader.buf.ascii());
    char *line = strtok(buf, "\n");

    // Everything up to the first newline is the one line comment.
    a->insert("summary", new QString(line));
    line = strtok(0, "\n");

    // Skip any blank lines and then read to EOF to get the description.
    while (line && line[0] == 0) line = strtok(0, "\n");

    QString *desc = new QString;
    while (line) {
      // Remove any new lines from the string. However, double ones should be preserved
      // as part of some semblence of an attempt to preserve the formatting...
      int line_len = strlen(line);
      if (line_len == 0) desc->append("\n");
      else {
	desc->append(QString(line));
	desc->append(" ");
      }

      line = strtok(0, "\n");
    }

    bsdPortsIndexItem *inditem = bsdPortsIndexItem::find(name);

    if (inditem) {
      installed = inditem->installed;
      a->insert("maintainer", new QString(inditem->maint));

      insertGroups(a,inditem->cats);

      a->insert("run depends", new QString(inditem->rdeps[0]? QString(inditem->rdeps) : i18n("none")));
      a->insert("build depends", new QString(inditem->bdeps[0]? QString(inditem->bdeps) : i18n("none")));
      a->insert("available as", new QString(inditem->bin ? (inditem->port? i18n("binary package and source port") : i18n("binary package")) : i18n("source port")));
    }

    a->insert("description", desc);
    free(buf);
  } else {
    kpackage->setStatus(QString::null);
    return 0;
  }

  packageInfo *ret = new packageInfo(a, this);
  ret->packageState = installed? packageInfo::INSTALLED : packageInfo::AVAILABLE;
  ret->fixup();
  if (!installed) ret->smerge(typeID);
  kpackage->setStatus(QString::null);
  return ret;
}

QStringList fbsdInterface::getChangeLog(packageInfo *) {
  return 0;
}


bool fbsdInterface::filesTab(packageInfo *) {
  return TRUE;
}

bool fbsdInterface::changeTab(packageInfo *) {
    return FALSE;
}

QStringList fbsdInterface::getFileList(packageInfo *p) {

  // Run pkg_info on the package name to get the file list.
  // The file list is returned on stdout, one per line.
  kpackage->setStatus(i18n("Getting file list"));

  QStringList ret;

  // Find the full name 'name-version', or just 'name' if version is empty.
  // Check first that it is actually installed.
  QString name( p->getProperty("filename"));

  if (!name.isEmpty() && (p->packageState != packageInfo::INSTALLED)) {
    QString qbname( p->getProperty("base"));
    if (!qbname.isEmpty())
       name = qbname + "/" + name;
  } else {
    if (!p->hasProperty("name")) {
      ret.append(i18n("Can't find package name!"));
      kpackage->setStatus(QString::null);
      return ret;
    }

    name = p->getProperty("name");

    QString version( p->getProperty("version"));
    if (!version.isEmpty()) {
       name = name + "-" + version;
    }
  }

  // Open a pipe to a pkg_info process in order to read the file list.
  // This works for both installed packages and for files.
  reader.setup(PKG_INFO_BIN);
  *reader.proc << "-L" << "-q" << name;

  if (reader.start(0)) {
    const char *op = reader.buf.ascii();
    if (op == 0) {
      ret.append(i18n("pkg_info returned no output"));
      kpackage->setStatus(QString::null);
      return ret;
    }
    char *buf = strdup(op);
    char *line = strtok(buf, "\n");
    while (line != 0) {
      ret.append(line);
      line = strtok(0, "\n");
    }
    free(buf);
  } else {
    ret.append(i18n("Can't start pkg_info"));
  }

  kpackage->setStatus(QString::null);
  return ret;
}


//  QPtrList<char> *verify(packageInfo *p, QPtrList<char> *files);

QString fbsdInterface::doUninstall(int uninstallFlags, QString packs, bool &)
{

  QString s = PKG_DELETE_BIN;
  s += " ";
  s += setOptions(uninstallFlags, paramsUninst);
  s += packs;

  kdDebug() << "uCMD=" << s << "\n";

  return  s;
}


QString fbsdInterface::doInstall(int installFlags, QString packs, bool &)
{

  QString s = PKG_ADD_BIN;
  s += " ";
  s += setOptions(installFlags, paramsInst);
  s += packs;

  kdDebug() << "iCMD=" << s << "\n";

  return  s;
}

QString fbsdInterface::uninstall(int uninstallFlags, packageInfo *p, bool &test)
{
  QString packs( p->getProperty("name"));
  QString vers( p->getProperty("version"));
  if (vers.length() > 0) packs += "-" + vers;

  return doUninstall(uninstallFlags, packs, test);
}

QString fbsdInterface::uninstall(int uninstallFlags, QPtrList<packageInfo> *p, bool &test)
{
  QString packs ;
  packageInfo *i;

  for (i = p->first(); i!= 0; i = p->next())  {
    packs += i->getProperty("name");
    QString vers( i->getProperty("version"));
    if (vers.length() != 0) packs += "-" + vers;
    packs += " ";
  }
  return doUninstall( uninstallFlags, packs, test);
}

QStringList fbsdInterface::FindFile(const QString &) {
  QStringList tmp;
  return tmp;
}

bool fbsdInterface::parseName(const QString &name, QString *n, QString *v) {
  int m1;

  m1 = name.findRev('-');
  if (m1 <= 0) return false;
  *n = name.left(m1);
  *v = name.right(name.length() - m1 - 1);
  return true;
}

void fbsdInterface::addNV(QDict<QString> *d, const QString &name) {
  QString *n = new QString;
  QString *v = new QString;

  if (!parseName(name, n, v)) {
    *n = name;
    *v = QString::null;
  }

  d->insert("name", n);
  d->insert("version", v);
}

  //public slots
void fbsdInterface::setLocation() {
  locatedialog->restore();
}

void fbsdInterface::setAvail(LcacheObj *slist) {
  kdDebug() << k_funcinfo << endl;
  
  if (packageLoc) delete packageLoc;
  packageLoc = slist;

  cacheObj *cp = packageLoc->first();
  bool ports_found = false; // Set to true when the ports tree config is found.

  if (cp && !cp->location.isEmpty()) {
    for (; cp != 0; cp = packageLoc->next()) {
      ports_found = cp->cacheFile == "BSD_0_0";

      QString oldloc = cp->location;
      cp->location += "/INDEX";
      QString s = getPackList(cp);
      if (!s.isEmpty()) bsdPortsIndexItem::processFile(QFile::encodeName(s), cp->cacheFile != "BSD_0_0", oldloc);
      cp->location = oldloc;
    }
  }

  if (!ports_found) {
    // Try the standard ports tree locations.
    bsdPortsIndexItem::processFile("/usr/ports/INDEX", false, "/usr/ports"); // FreeBSD/OpenBSD
    bsdPortsIndexItem::processFile("/usr/opt/INDEX", false, "/usr/opt");  // NetBSD
  }
}


void fbsdInterface::listPackages(QPtrList<packageInfo> *pki) {
  int listscan;

  kdDebug() << k_funcinfo << endl;
  
  listInstalledPackages(pki);
  
  unsigned int hashcount = 0;
  for (listscan = 0; listscan < 256; listscan++) {
    bsdPortsIndexItem *scan = bsdPortsIndexItem::lists[listscan];
    if (scan) hashcount++;
    while (scan != 0) {
      if (!scan->installed  /*&& scan->bin */) {
	QDict<QString> *a = new QDict<QString>;
	a->setAutoDelete(TRUE);

	addNV(a, scan->name);
	a->insert("summary", new QString(scan->name));
	a->insert("maintainer", new QString(scan->maint));

	insertGroups(a,scan->cats);

        a->insert("run depends", new QString(scan->rdeps[0]? QString(scan->rdeps) : i18n("none")));
        a->insert("build depends", new QString(scan->bdeps[0]? QString(scan->bdeps) : i18n("none")));
	a->insert("available as", new QString(scan->bin ? (scan->port? i18n("binary package and source port") : i18n("binary package")) : i18n("source port")));

	a->insert("filename", new QString(scan->bin_filename));
	a->insert("base", new QString(scan->bin_filename_base));

	packageInfo *info = new packageInfo(a, this);
	info->packageState = packageInfo::AVAILABLE;
	info->smerge(typeID);
	info->fixup();
	info->pkgInsert(pki, typeID, false);
//	pki->append(info);
      }

      scan = scan->next;
    }
  }
  
  kdDebug() << "There were " << hashcount << " occupied buckets." << endl;
  kdDebug() << "There are " << pki->count() << " total ports." << endl;
}

void fbsdInterface::listInstalledPackages(QPtrList<packageInfo> *pki) {
  kdDebug() << k_funcinfo << endl;
  
#define INFO_SEPARATOR "f;g#z-@IqbX%"

  // Open a pipe to a pkg_info process in order to read the comment, name
  // and description for the packages.

  kpackage->setStatus(i18n("Querying BSD packages database for installed packages"));

  reader.setup(PKG_INFO_BIN);
  *reader.proc << "-acdl" << INFO_SEPARATOR;
  if (!reader.start(0, FALSE)) {
    kpackage->setStatus(QString::null);
    return;
  } else {
    if (reader.buf.isEmpty()) {
      kpackage->setStatus(QString::null);
      return;
    }
    // We should now get:
    //  INFO_SEPARATORInformation for pkgname:
    //
    //  INFO_SEPARATORComment:
    //   <one line description>
    //
    //  INFO_SEPARATORDescription:
    //   <description>
    //
    //
    //  INFO_SEPARATOR
    //  INFO_SEPARATORInformation for [etc]

    char *buf = strdup(reader.buf.ascii());
    char *line = strtok(buf, "\n");

    while (line != 0) {
      QDict<QString> *a = new QDict<QString>;
      a->setAutoDelete(TRUE);

      if (memcmp(line, INFO_SEPARATOR, sizeof(INFO_SEPARATOR)-1)) {
	KpMsgE(i18n("Unexpected output from pkg_info: %1").arg(line), TRUE);
        kpackage->setStatus(QString::null);
	return;
      }

      // Find the last word on this line (which should be the package name) minus a trailing :.
      char *lastword = strrchr(line, ' ');
      int lastword_len = lastword? strlen(lastword) : 0;
      if (lastword_len < 2 || lastword[lastword_len-1] != ':') {
	KpMsgE(i18n("Unexpected output from pkg_info (looking for package name): %1").arg(line), TRUE);
        kpackage->setStatus(QString::null);
	return;
      }

      lastword[lastword_len-1] = 0;
      addNV(a, lastword+1);

      // Now find the package comment. Skip lines up to the next separator.
      bool done = true;
      do {
	// If not the first time round and we've found the separator then exit the loop when we reach the end.
	if (!done && !memcmp(line, INFO_SEPARATOR, sizeof(INFO_SEPARATOR)-1)) done = 1;
	else done = false;

	line = strtok(0, "\n");
	if (line == 0) {
	  KpMsgE(i18n("Unexpected EOF from pkg_info (looking for comment line)"), TRUE);
          kpackage->setStatus(QString::null);
	  return;
	}
      } while (!done);

      a->insert("summary", new QString(line));

      // Now look for the package description. Skip to the next separator.
      // The current line doesn't contain one this time.
      do {
	line = strtok(0, "\n");
	if (line == 0) {
	  KpMsgE(i18n("Unexpected EOF from pkg_info (looking for comment line)"), TRUE);
          kpackage->setStatus(QString::null);
	  return;
	}
      } while (memcmp(line, INFO_SEPARATOR, sizeof(INFO_SEPARATOR)-1));

      // Read to INFO_SEPARATOR to get the description.
      QString *desc = new QString;

      line = strtok(0, "\n");
      while (line != 0 && memcmp(line, INFO_SEPARATOR, sizeof(INFO_SEPARATOR)-1)) {
	// Remove any new lines from the string. However, double ones should be preserved
	// as part of some semblence of an attempt to preserve the formatting...
	int line_len = strlen(line);
	if (line_len == 0) desc->append("\n");
	else {
	  desc->append(QString(line));
	  desc->append(" ");
	}

	line = strtok(0, "\n");
      }

      bsdPortsIndexItem *inditem = bsdPortsIndexItem::find(lastword+1);

      if (inditem) {
	inditem->installed = true;

	a->insert("maintainer", new QString(inditem->maint));

	insertGroups(a,inditem->cats);
	if (!a->find("group"))
	{
          kdDebug() << "Line <" << buf << "> has no group?" << endl;
        }

        a->insert("run depends", new QString(inditem->rdeps[0]? QString(inditem->rdeps) : i18n("none")));
        a->insert("build depends", new QString(inditem->bdeps[0]? QString(inditem->bdeps) : i18n("none")));
	a->insert("available as", new QString(inditem->bin ? (inditem->port? i18n("binary package and source port") : i18n("binary package")) : i18n("source port")));
      }

      a->insert("description", desc);

      int pkg_state = packageInfo::INSTALLED;
      if (!a->find("group"))
      {
        QString s,*ps;
        ps = a->find("name");
        if (ps) s = *ps;
        else s = "<anonymous>";

        ps = a->find("version");
        if (ps) s.append(QString("-")+(*ps));

        kdDebug() << "Package " << (s) << " has no group." << endl;
 
        /* This must be an installed package with no INDEX entry,
        ** which usually means that the port has been updated.
        */
        reader.setup(PKG_INFO_BIN);
        *reader.proc << "-o" << (s);
        if (reader.start(QString::null,FALSE) && !reader.buf.isEmpty())
        {
          QStringList lines = QStringList::split('\n',reader.buf);
          QString originName;
          int originOffset = lines.findIndex(QString("Origin:"));
          if (originOffset>=0) originName=lines[originOffset+1];
          int slashOffset = -1;
          if(!originName.isEmpty()) slashOffset=originName.find('/');
          if(slashOffset>=0) originName.truncate(slashOffset);
          if(!originName.isEmpty()) a->insert("group",new QString(originName));
          pkg_state = packageInfo::UPDATED;
        }
        else
        {
          kdDebug() << "Could not read package origin info." << endl;
        }
      }

      packageInfo *info = new packageInfo(a, this);
      info->packageState = pkg_state;

      info->fixup();
      //pki->append(info);
      info->pkgInsert(pki, typeID, true);

      if (line != 0) line = strtok(0, "\n");
      }
    free(buf);
    }

#undef INFO_SEPARATOR
}


bsdPortsIndexItem::bsdPortsIndexItem(char *desc, bool binaries, QString dname) : bin(binaries), port(!binaries), installed(false) {
  int itemno;

  rdeps = ""; // Could otherwise end up unintialised.
  for (itemno = 0; *desc != 0; itemno++) {
    switch (itemno) {
    case 0:
      name = desc;
      break;
    case 1:
      path = desc;
      break;
    case 2:
      prefix = desc;
      break;
    case 3:
      comment = desc;
      break;
    case 4:
      desc_path = desc;
      break;
    case 5:
      maint = desc;
      break;
    case 6:
      cats = desc;
      break;
    case 7:
      bdeps = desc;
      break;
    case 8:
      rdeps = desc;
      break;
    default:
      ;
    }

    while (*desc != 0 && *desc != '|') desc++;

    if (*desc == 0) break;

    *desc = 0;
    desc++;
  }

  if (itemno != 8 && itemno != 9) {
     KpMsgE(i18n("Warning: invalid INDEX file entry for %1").arg(name),FALSE);
  }
    name_hash = calc_hash4(name);

    // Order the entries by hash.
    bsdPortsIndexItem **scan = &lists[hash1(name_hash)];
    while (*scan && (*scan)->name_hash < name_hash) scan = &((*scan)->next);

    if (*scan && (*scan)->name_hash == name_hash && !strcmp((*scan)->name, name)) {
      // Collision. May be port and package.
      (*scan)->bin = (*scan)->bin || bin;
      (*scan)->port = (*scan)->port || port;
      if (binaries) {
	(*scan)->bin_filename = QString(name) + ".tgz";
	(*scan)->bin_filename_base = dname + "/";
       }
      name = 0; // Acts as a 'not used' tag.
      return;
    }

    if (binaries) {
      bin_filename = QString(name) + ".tgz";
      bin_filename_base = dname + "/";
     }

    next = *scan;
    *scan = this;
}

bsdPortsIndexItem *bsdPortsIndexItem::lists[256];

unsigned int bsdPortsIndexItem::calc_hash4(const char *pname) {
  unsigned int ret = 0;
  const char *name = pname;
  while (*name != 0) {
    ret += (*name) << (((name - pname) & 3) *8);
    name++;
  }
  return ret;
}

unsigned char bsdPortsIndexItem::calc_hash1(const char *name) {
  return hash1(calc_hash4(name));
}

unsigned char bsdPortsIndexItem::hash1(unsigned int h) {
  return ((h & 0x03000000) >> 24) | ((h & 0x0c0000) >> 16) | ((h & 0x3000) >> 8) | (h & 0xc0);
}

void bsdPortsIndexItem::processFile(const QString &fname, bool binaries, const QString &dname) {
  // Read the file in to a buffer and null terminate it.

  struct stat s;

  if (stat(fname.ascii(), &s) == -1) {
    // Error message?
    return;
  }

  char *index = (char *) malloc(s.st_size);
  int fd;

  fd = open(fname.ascii(), O_RDONLY);
  if (fd == -1) {
    // Error message?
    return;
  }

  int size = read(fd, index, s.st_size);
  index[size] = 0;
  close(fd);


  // Go through each line and create a new bsdPortsIndexItem.
  char *line = strtok(index, "\n");
  while (line != 0) {
     bsdPortsIndexItem *i = new bsdPortsIndexItem(line, binaries, dname + "/All/");
     if (i->name == 0) delete i;
    line = strtok(0, "\n");
  }
}

bsdPortsIndexItem *bsdPortsIndexItem::find(const QString &name) {
  if (name.isEmpty()) return 0;

  unsigned int hash = calc_hash4(name.ascii());

  bsdPortsIndexItem *scan = lists[hash1(hash)];

  while (scan != 0) {
    if (scan->name_hash == hash || true) {
      if (!strcmp(name.ascii(), scan->name)) return scan;
    }
    scan = scan->next;
  }

  return 0;
}

#include "fbsdInterface.moc"
