#include <errno.h>
#include <fcntl.h>
#include <newt.h>
#include <rpmlib.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>		/* for mkdir(2) ?!? */
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/signal.h>
#include <unistd.h>
#include <popt.h>

#include "setup.h"
#include "doit.h"
#include "install.h"
#include "intl.h"
#include "kbd.h"
#include "kickstart.h"
#include "log.h"
#include "syslog.h"
#include "windows.h"

#define FILESIZE_TAG 1000001

extern int testing;
extern int_32 archiveSize;
extern pid_t syncPid;
extern pid_t syslogPid;
FILE * logFile = NULL;

static void rpmerror(void);

static void swOpen(int numPackages, int sizePackages);
static void swPackage(Header h);
static void swPackageComplete();
static void swClose(void);
static void * swCallback(const Header h, const rpmCallbackType what, 
			 const unsigned long amount, const unsigned long total,
			 const void * pkgKey, void * data);
static void formatTime(char * buf, time_t t);
static int probSort(const void * one, const void * two);

struct callbackInfo {
    struct packageInfo * pkg;
    int upgrade;
    struct installMethod * method;
    FD_t fd;
    char *lastFile;
    int archiveSize;
    int step;
    newtComponent statusForm, statusScale;
    int totalnum, totalsize;
};

static struct statusWindowInfo {
    newtComponent form, packageLabel, sizeLabel, summaryText;
    newtComponent pkgScale, globalScale;
    newtComponent pkgDoneLabel, pkgRemainsLabel;
    newtComponent sizeDoneLabel, sizeRemainsLabel;
    newtComponent timeDoneLabel, timeRemainsLabel, timeTotalLabel;
    int numPackages, packagesDone;
    unsigned int sizePackages, sizeDone;
    int thisPackageSize;
    time_t timeStarted;
} si;

int doInstall(struct installMethod * method, char * rootPath,
	      struct pkgSet * psp, char * netSharedPath, char * keymap,
	      char * kbdtype, int upgrade) {
    int i, totalNumPackages, totalSizePackages;
    rpmdb db;
    FD_t lfFd;
    char * path;
    rpmTransactionSet trans;
    rpmProblemSet probs;
    char * hostEntry = 
		"127.0.0.1		localhost localhost.localdomain\n";
    int fd;
    struct callbackInfo cbi;
    char *procPath;
    
    if (testing) return 0;

    path = alloca(strlen(rootPath) + 20);

    fd = open(".", O_RDONLY);
    chdir(rootPath);
    mkdir("dev", 0755);
    mkdir("etc", 0755);
    mkdir("tmp", 0755);
    mkdir("var", 0755);
    mkdir("var/tmp", 0755);
    mkdir("var/lib", 0755);
    mkdir("var/lib/rpm", 0755);
    mkdir("proc", 0755);
    fchdir(fd);
    close(fd);

#ifdef __sparc__
	logMessage("removing /mnt/usr/X11R6 symlink");
	unlink("/mnt/usr/X11R6");
#endif

    handleSyslogSocket(rootPath);
	
    if (!upgrade) {
#ifdef __sparc__
	start_openprom();
#endif
	sprintf(path, "%s/etc/hosts", rootPath);
	fd = open(path, O_CREAT | O_RDWR, 0644);
	if (fd < 0) {
	    errorWindow("Failed to create etc/hosts: %s.");
	    return INST_ERROR;
	}

	write(fd, hostEntry, strlen(hostEntry));
	close(fd);
    }

    sprintf(path, "%s/tmp/%s.log", rootPath, upgrade ? "upgrade" : "install");

    lfFd = fdOpen(path, O_RDWR | O_CREAT | O_TRUNC, 0600);

    if (lfFd) {
	logFile = fdopen(fdFileno(lfFd), "w");
	setlinebuf(logFile);
	logMessage("opened %s", path);
    } else {
	logMessage("failed to open %s :-(", path);
	newtWinMessage(_("Error"), _("Ok"),
		_("Failed to open %s. No upgrade log will be kept."), path);
    }

    rpmErrorSetCallback(rpmerror);
    
    /* FIXME: we ought to read /mnt/us/lib/rpmrc if we're in the midst of an
       upgrade, but it's not obvious how to get RPM to do that. */
    /* if we set netshared path to "" then we get no files installed
    addMacro(&globalMacroContext, "_netsharedpath", NULL,
	     netSharedPath ? netSharedPath : "" , RMIL_RPMRC);
    */
    
    rpmdbInit(rootPath, 0644);

    if (rpmdbOpen(rootPath, &db, O_RDWR | O_CREAT, 0644)) {
	errorWindow(_("Fatal error opening RPM database"));
	return INST_ERROR;
    }
    logMessage("opened rpm database");

    trans = rpmtransCreateSet(db, rootPath);

    for (i = 0; i < psp->numPackages; i++) {
	if (!strcmp(psp->packages[i]->name, "basesystem")) {
	    rpmtransAddPackage(trans, psp->packages[i]->h, NULL,
			       psp->packages[i], upgrade, NULL);
	}
    }

    totalNumPackages = 0, totalSizePackages = 0;
    for (i = 0; i < psp->numPackages; i++) {
	if (psp->packages[i]->selected) {
	    if (strcmp(psp->packages[i]->name, "basesystem")) {
		rpmtransAddPackage(trans, psp->packages[i]->h, NULL,
				 psp->packages[i], upgrade, NULL);
	    }

	    totalSizePackages += psp->packages[i]->size;
	    totalNumPackages++;
	}
    }

    if (rpmdepOrder(trans)) {
	rpmdbClose(db);
	rpmtransFree(trans);
	newtWinMessage(_("Error"), _("Ok"), 
		       _("Error ordering package list: %s"), rpmErrorString());
	return 1;
    }

    rpmtransSetScriptFd(trans, lfFd);

    cbi.step = -1;

    if (testing) {
	rpmtransFree(trans);
	newtWinMessage("Status", "Ok", "Packages would be installed now");
	return 0;
    }

    cbi.method = method;
    cbi.upgrade = upgrade;
    cbi.totalnum = totalNumPackages;
    cbi.totalsize = totalSizePackages;
    si.packagesDone = -1;

    /* mount /proc in the instroot */
    procPath = alloca(strlen(rootPath) + 50);
    sprintf(procPath, "%s/proc", rootPath);
    umount(procPath);
    if (doMount("/proc", procPath, "proc", 0, 0)) {
	return INST_ERROR;
    }
    if (rpmRunTransactions(trans, swCallback, &cbi, NULL, &probs, 0, 
		           ~RPMPROB_FILTER_DISKSPACE) && probs->numProblems) {
	char buf[2048], sbuf[100];
	int i, last, rc, size;

	/* must not have enough disk space :-( */
	qsort(probs->probs, probs->numProblems, sizeof(*probs->probs),
	      probSort);

	strcpy(buf, _("You don't appear to have enough disk space to install "
		"the packages you've selected. You need more space on the "
		"following filesystems:\n\n"));

	size = 12; /* Minimum padding on filesystem */
	for (i = 0; i < probs->numProblems; i++) {
	    if (strlen(probs->probs[i].str1) > size)
		size = strlen(probs->probs[i].str1);
	}
	
	sprintf(sbuf, "     %-*s  %s\n", size, _("Mount Point"),
		_("Space Needed"));
	strcat(buf, sbuf);
	
	last = -1;
	for (i = 0; i < probs->numProblems; i++) {
	    if (last == -1 || strcmp(probs->probs[last].str1, 
				     probs->probs[i].str1)) {
		path = probs->probs[i].str1;
		/* knock off the leading /mnt */
		if (!strncmp("/mnt", path, 4))
		   path += 4;
		sprintf(sbuf, "     %-*s  %ld%c\n", size,
			path,
			probs->probs[i].ulong1 > (1024*1024) ?
			(probs->probs[i].ulong1 + 1024 * 1024 - 1) / (1024 * 1024) :
			(probs->probs[i].ulong1 + 1023) / 1024,
			probs->probs[i].ulong1 > (1024*1024) ? 'M' : 'k',
			probs->probs[i].ulong1);
		strcat(buf, sbuf);
		last = i;
	    }
	}

	rpmProblemSetFree(probs);

	rc = newtWinChoice(_("Disk Space"), _("Back"), _("Install anyway"),
			   buf);
	
	if (rc != 2) {
	    rpmdbClose(db);
	    rpmtransFree(trans);
	    if (syncPid > 0) {
		kill(syncPid, SIGKILL);
		waitpid(syncPid, NULL, 0);
	    }
	    if (syslogPid > 0) {
		kill(syslogPid, SIGKILL);
		waitpid(syslogPid, NULL, 0);
	    }
	    fdClose(lfFd);
	    umount(procPath);
	    return INST_CANCEL;
	}
	rpmRunTransactions(trans, swCallback, &cbi, NULL, &probs, 0, ~0);
	
    }
    umount(procPath);

    if (syncPid > 0) {
	kill(syncPid, SIGKILL);
	waitpid(syncPid, NULL, 0);
    }

    fdClose(lfFd);

    swClose();

    rpmProblemSetFree(probs);
    
    rpmtransFree(trans); 

    rpmdbClose(db);

    logMessage("rpm database closed");

    if (!upgrade) {
	sprintf(path, "%s/etc/sysconfig", rootPath);
	mkdir(path, 0755);
	writeKbdConfig(path, keymap, kbdtype);
    }

    if (!upgrade) {
	writeLangInfo(rootPath);
    }

    if (logFile) fclose(logFile);
    logFile = NULL;

    return 0;
}

int setupXfree(struct installMethod * method, char * rootPath,
	       struct pkgSet * psp) {
    int fd, i;
    char buf[200], * chptr;
    char server[50];
    int rc;
    char * path;
    char * procPath;
    rpmdb db;
    rpmTransactionSet trans;
    struct callbackInfo cbi;
    rpmProblemSet probs;

    if (rpmdbOpen(rootPath, &db, O_RDWR | O_CREAT, 0644)) {
	errorWindow(_("Fatal error reopening RPM database"));
	return INST_ERROR;
    }
    logMessage("reopened rpm database");

    path = alloca(strlen(rootPath) + 200);
    procPath = alloca(strlen(rootPath) + 50);
    sprintf(path, "%s/usr/X11R6/bin/Xconfigurator", rootPath);

    /* This is a cheap trick to see if our X component was installed */
    if (access(path, X_OK)) {
	logMessage("%s cannot be run", path);
	return INST_OKAY;
    }

    /* need proc to do pci probing */
    sprintf(procPath, "%s/proc", rootPath);
    umount(procPath);
    if ((rc = doMount("/proc", procPath, "proc", 0, 0))) {
	return INST_ERROR;
    }

    /* this handles kickstart and normal/expert modes */
    if ((rc=xfree86Config(rootPath, "--pick")))
	return INST_ERROR;
    
    sprintf(path, "%s/tmp/SERVER", rootPath);
    if ((fd = open(path, O_RDONLY)) < 0) {
	logMessage("failed to open %s: %s", path, strerror(errno));
	return INST_ERROR;
    }
 
    buf[0] = '\0';
    read(fd, buf, sizeof(buf));
    close(fd);
    chptr = buf;
    while (chptr < (buf + sizeof(buf) - 1) && *chptr && *chptr != ' ')
	chptr++;

    if (chptr >= (buf + sizeof(buf) - 1) || *chptr != ' ') {
	logMessage("couldn't find ' ' in %s", path);
	return INST_ERROR;
    }

    *chptr = '\0';
    strcpy(server, "XFree86-");
    strcat(server, buf);

    logMessage("I will install the %s package", server);

    for (i = 0; i < psp->numPackages; i++) {
	if (!strcmp(psp->packages[i]->name, server)) {
	    logMessage("\tfound package: %s", psp->packages[i]->name);
	    swOpen(1, psp->packages[i]->size);
	    trans = rpmtransCreateSet(db, rootPath);
	    rpmtransAddPackage(trans, psp->packages[i]->h, NULL,
			       psp->packages[i], 0, NULL);
	    
	    cbi.method = method;
	    cbi.upgrade = 0;
	    
	    rpmRunTransactions(trans, swCallback, &cbi, NULL, &probs, 0, 
				0xffffffff);
	    
	    swClose();
	    break;
	}
    }

    /* this handles kickstart and normal/expert modes */
    if ((rc=xfree86Config(rootPath, "--continue")))
	return INST_ERROR;

    /* done with proc now */
    umount(procPath);

    rpmdbClose(db);

    logMessage("rpm database closed");

    return INST_OKAY;
}

static void rpmerror(void) {
    int code;

    code = rpmErrorCode();
    if (code != RPMERR_UNLINK && code != RPMERR_RMDIR) {
	if (logFile)
	    fprintf(logFile, "%s\n", rpmErrorString());
	else
	    logMessage(rpmErrorString());
    }
}

static void swOpen(int numPackages, int sizePackages) {
    char buf[50];

    newtCenteredWindow(60, 15, _("Install Status"));

    si.form = newtForm(NULL, NULL, 0);
    newtFormAddComponent(si.form, newtLabel(1, 1, "Package:"));
    newtFormAddComponent(si.form, newtLabel(1, 2, "Size   :"));
    newtFormAddComponent(si.form, newtLabel(1, 3, "Summary:"));

    si.packageLabel = newtLabel(13, 1, "");
    si.sizeLabel    = newtLabel(13, 2, "");
    si.summaryText  = newtTextbox(13, 3, 45, 2, NEWT_TEXTBOX_WRAP);

    si.pkgScale = newtScale(3, 6, 54, 100);

    newtFormAddComponent(si.form, 
	newtLabel(1, 8, "             Packages       Bytes           Time"));
	/*		 12345678901234567890123456789012345678901234567
				  1         2         3         4 */
    newtFormAddComponent(si.form, newtLabel(1, 9,  "Total     :"));
    newtFormAddComponent(si.form, newtLabel(1, 10, "Completed :"));
    newtFormAddComponent(si.form, newtLabel(1, 11, "Remaining :"));

    si.numPackages = numPackages;
    si.sizePackages = sizePackages;
    si.packagesDone = 1;
    si.sizeDone = 0;
    si.timeStarted = time(NULL);

    sprintf(buf, "%8d", numPackages);
    newtFormAddComponent(si.form, newtLabel(14, 9, buf));
    si.pkgDoneLabel = newtLabel(14, 10, "");
    si.pkgRemainsLabel = newtLabel(14, 11, "");

    sprintf(buf, "%4uM", sizePackages / (1024 * 1024));
    newtFormAddComponent(si.form, newtLabel(29, 9, buf));
    si.sizeDoneLabel = newtLabel(29, 10, "");
    si.sizeRemainsLabel = newtLabel(29, 11, "");

    si.timeTotalLabel = newtLabel(42, 9, "");
    si.timeDoneLabel = newtLabel(42, 10, "");
    si.timeRemainsLabel = newtLabel(42, 11, "");

    if (sizePackages)
	si.globalScale = newtScale(1, 13, 58, sizePackages);
    else
	si.globalScale = newtScale(1, 13, 58, 1);

    newtFormAddComponents(si.form, si.packageLabel, si.sizeLabel, 
			  si.summaryText, si.pkgScale, si.globalScale, 
			  si.pkgDoneLabel, si.pkgRemainsLabel, 
			  si.sizeDoneLabel, si.sizeRemainsLabel, 
			  si.timeDoneLabel, si.timeRemainsLabel,
			  si.timeTotalLabel, NULL);
}

static void swPackage(Header h) {
    char * name, * version, * release, * summary;
    char buf[50];
    uint_32 * size;

    headerGetEntry(h, RPMTAG_NAME, NULL, (void *) &name, NULL);
    headerGetEntry(h, RPMTAG_VERSION, NULL, (void *) &version, NULL);
    headerGetEntry(h, RPMTAG_RELEASE, NULL, (void *) &release, NULL);
    headerGetEntry(h, RPMTAG_SIZE, NULL, (void *) &size, NULL);

    if (!headerGetEntry(h, RPMTAG_SUMMARY, NULL, (void *) &summary, NULL))
	summary = _("(no summary)");

    sprintf(buf, "%s-%s-%s", name, version, release);
    newtLabelSetText(si.packageLabel, buf);

    sprintf(buf, "%dk", (*size) / 1024);
    newtLabelSetText(si.sizeLabel, buf);

    newtTextboxSetText(si.summaryText, summary);

    si.thisPackageSize = *size;

    newtScaleSet(si.pkgScale, 0);

    newtDrawForm(si.form);
    newtRefresh();
}

static void swPackageComplete(void) {
    char buf[50];
    time_t now, finishTime, elapsedTime, remainingTime;

    if (si.packagesDone > si.numPackages)
	return;
    
    sprintf(buf, "%8d", si.packagesDone);
    newtLabelSetText(si.pkgDoneLabel, buf);

    sprintf(buf, "%8d", si.numPackages - si.packagesDone);
    newtLabelSetText(si.pkgRemainsLabel, buf);

    sprintf(buf, "%4dM", si.sizeDone / (1024 * 1024));
    newtLabelSetText(si.sizeDoneLabel, buf);

    sprintf(buf, "%4dM", (si.sizePackages - si.sizeDone) / (1024 * 1024));
    newtLabelSetText(si.sizeRemainsLabel, buf);

    si.packagesDone++;
    si.sizeDone += si.thisPackageSize;
    
    now = time(NULL);
    elapsedTime = now - si.timeStarted;
    formatTime(buf, elapsedTime);
    newtLabelSetText(si.timeDoneLabel, buf);

    if (si.sizeDone == 0)
      finishTime = 0;
    else
      finishTime = (((float) si.sizePackages) / si.sizeDone) * elapsedTime;
    formatTime(buf, finishTime);
    newtLabelSetText(si.timeTotalLabel, buf);
    
    remainingTime = finishTime - elapsedTime;
    formatTime(buf, remainingTime);
    newtLabelSetText(si.timeRemainsLabel, buf);

    newtScaleSet(si.globalScale, si.sizeDone);

    newtRefresh();
}

static void * swCallback(const Header h, const rpmCallbackType what, 
			 const unsigned long amount, const unsigned long total,
			 const void * pkgKey, void * data) {
    void * rc = NULL;
    struct callbackInfo * cbi = data;
    struct packageInfo * pkg = (void *) pkgKey;
    char * realName;
    int olderrno, i;
    int_32 *iptr;
    static int haveWindow = 0;

    struct {
	int num;
	char *desc;
    } descr[] = {
	{ 1, _("Examining packages to install...")},
	{ 5, _("Examining files to install...")},
	{ 6, _("Finding overlapping files...")},
	{ -1, NULL }
    };
    
    switch (what) {
      case RPMCALLBACK_TRANS_START:
      case RPMCALLBACK_UNINST_START:
	if (amount < 9) {
	    i = 0;
	    while(descr[i].num != -1 && descr[i].num != amount)
		i++;
	    if (descr[i].num != -1 || what == RPMCALLBACK_UNINST_START) {
		newtCenteredWindow(60, 5, _("Processing"));
		haveWindow = 1;
		cbi->statusForm = newtForm(NULL, NULL, 0);

		newtFormAddComponent(cbi->statusForm,
				     newtLabel(1, 1,
				       what == RPMCALLBACK_TRANS_START ?
					       descr[i].desc :
					  _("Removing old files...")));
		cbi->statusScale = newtScale(1, 3, 58, total);
		newtFormAddComponent(cbi->statusForm, cbi->statusScale);
		newtDrawForm(cbi->statusForm);
		newtRefresh();
	    }
	}
	break;

      case RPMCALLBACK_TRANS_PROGRESS:
      case RPMCALLBACK_UNINST_PROGRESS:
	if (haveWindow) {
	    newtScaleSet(cbi->statusScale, amount);
	    newtRefresh();
	}
	break;
	
      case RPMCALLBACK_TRANS_STOP:
      case RPMCALLBACK_UNINST_STOP:
        if (haveWindow) {
	    newtFormDestroy(cbi->statusForm);
	    newtPopWindow();
	    haveWindow = 0;
	}
	break;
	
      case RPMCALLBACK_INST_START:
	swPackage(pkg->h);
	break;

      case RPMCALLBACK_INST_PROGRESS:
	if (total == 0)
	    newtScaleSet(si.pkgScale, 100);
	else
	    newtScaleSet(si.pkgScale, (amount * 100) / total);
	newtRefresh();
	break;

      case RPMCALLBACK_INST_CLOSE_FILE:
	swPackageComplete();
	fdClose(cbi->fd);
	if (cbi->method->rmFiles) unlink(cbi->lastFile);
	break;

      case RPMCALLBACK_INST_OPEN_FILE:
	if (si.packagesDone == -1)
	    swOpen(cbi->totalnum, cbi->totalsize);
    
	if (logFile)
	    fprintf(logFile, "%s %s.\n", 
		    cbi->upgrade ? _("Upgrading") : _("Installing"), pkg->name);

	swPackage(pkg->h);
	
	if (!headerGetEntry(pkg->h, FILESIZE_TAG, NULL,
			    (void *) &iptr, NULL))
	    archiveSize = 0;
	else
	    archiveSize = *iptr;
	
	if (cbi->method->getFile(cbi->method, pkg->data, &realName)) {
	    logMessage("getFile method failed for %s", pkg->data);
	    if (logFile)
		fprintf(logFile, "Failed to get file for package %s.\n", 
			pkg->name);
	    swPackageComplete();
	    return NULL;
	}

	cbi->fd = fdOpen(realName, O_RDONLY, 0666);
	if (!cbi->fd) {
	    olderrno = errno;
	    logMessage("cannot open RPM file %s: %s", pkg->data,
			strerror(olderrno));
	    newtWinMessage(_("Error"), _("Ok"), 
			    _("Error installing package: cannot open RPM file "
			    "for %s: %s"), pkg->data, strerror(errno));
	    if (logFile)
		fprintf(logFile, "\tcannot open RPM file %s: %s\n", 
			    (char *) pkg->data, strerror(olderrno));

	    swPackageComplete();

	    return NULL;
	}

	cbi->lastFile = realName;
	rc = cbi->fd;
	
	break;
    }

    return rc;
}

static void swClose(void) {
    if (si.packagesDone > 0)
	newtPopWindow();
}

static void formatTime(char * buf, time_t t) {
    int hours, minutes, secs;

    hours = t / 60 / 60;
    t %= (60 * 60);

    minutes = t / 60;
    t %= 60;

    secs = t;

    sprintf(buf, "%01d:%02d.%02d", hours, minutes, secs);
}

static int probSort(const void * one, const void * two) {
    const rpmProblem * a = one;
    const rpmProblem * b = two;
    int rc;

    rc = strcmp(a->str1, b->str1);
    if (rc) return rc;

    /* this puts the largest need at the top! */
    if (a->ulong1 > b->ulong1)
	return -1;
    else if (a->ulong1 < b->ulong1)
	return 1;

    return 0;
}
