/*
 *      update.c
 *      
 *      Copyright 2008 Giorgio "Dani" G. <dani@slacky.it>
 *      
 *      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; if not, write to the Free Software
 *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *      MA 02110-1301, USA.
 */

#include "slackyd.h"

/* check for error on bzip2 decompression */
static bool
bzerror (const char *zpath, int bzval)
{
     switch (bzval) {

        case BZ_CONFIG_ERROR:
        	fprintf (stderr, "\nError on %s bzip decompression:\n", (zpath) ? zpath : "\b");
        	fprintf (stderr, "Bzip configuration error.\n"
        					 "Your library bzip has been improperly compiled on your platform.\n\n");
        	break;

        case BZ_SEQUENCE_ERROR:
        	fprintf (stderr, "\nError on %s bzip decompression:\n", (zpath) ? zpath : "\b");
        	fprintf (stderr, "Bzip sequence error call.\n"
        					 "Please report this.\n\n");
        	break;

        case BZ_PARAM_ERROR:
        	fprintf (stderr, "\nError on %s bzip decompression:\n", (zpath) ? zpath : "\b");
        	fprintf (stderr, "A parameter to a function call is out of range or manifestly incorrect.\n"
        					 "Please report this.\n");
        	break;

        case BZ_MEM_ERROR:
        	fprintf (stderr, "\nError on %s bzip decompression:\n", (zpath) ? zpath : "\b");
        	fprintf (stderr, "Bzip out of memory.\n\n");
        	break;

        case BZ_DATA_ERROR:
        	fprintf (stderr, "\nError on %s bzip decompression:\n", (zpath) ? zpath : "\b");
        	fprintf (stderr, "Bzip data integrity error is detected.\n\n");
        	break;

        case BZ_DATA_ERROR_MAGIC:
        	fprintf (stderr, "\nError on %s bzip decompression:\n", (zpath) ? zpath : "\b");
        	fprintf (stderr, "The compressed stream does not start with the correct magic bytes.\n\n");
        	break;

        case BZ_UNEXPECTED_EOF:
        	fprintf (stderr, "\nError on %s bzip decompression:\n", (zpath) ? zpath : "\b");
        	fprintf (stderr, "The compressed file finishes before the logical end of stream is detected.\n\n");
        	break;

        default:
        	return false;
        	break;
    }

    return true;
}
        

/* decompress file `zpath' in file `dest'
 * If `dest' is NULL uncompress file in `zpath' without extension.
 */
static bool
bz_decompress (const char *zpath, char *dest)
{
    char *zdata = NULL, *pdest = (dest) ? dest : xstrdup (zpath), *ptr;
    unsigned zsize;
    char buffer[BUFFSIZE];
    int rval;
    bz_stream zstream;
    FILE *fd;

    if (dest == NULL) {
        if ((ptr = strrchr (pdest, '.')) == NULL) {
            fprintf (stderr, "\nError: %s have no extension.\n", zpath);
            return true;
        }
        *ptr = 0;
    }
    
    /* load compressed data */
    if (!(fd = fopen (zpath, "rb"))) {
        fprintf (stderr, "\n"
        "Cannot decompress %s: %s\n", zpath, strerror (errno));
        return true;
    }
    zsize = get_file_size (fd);
    fdload (&zdata, fd, zsize);
    fclose (fd);
    
    if (zdata == NULL) {
        fprintf (stderr, "Error, %s seem to be void. Operation skipped.\n", zpath);
        return true;
    }
    
    /* initialize structure */
    zstream.total_in_lo32  = 0x00;
    zstream.total_in_hi32  = 0x00;
    zstream.total_out_lo32 = 0x00;
    zstream.total_out_hi32 = 0x00;
    zstream.bzalloc = NULL; /* use default alloc function */
    zstream.bzfree  = NULL; /* use default free function */
    zstream.opaque  = NULL;
    
    zstream.next_in   = zdata;    /* a pointer to compressed data */
    zstream.avail_in  = zsize;    /* compressed data size */
    zstream.next_out  = buffer;   /* temp buffer to save uncompressed data */
    zstream.avail_out = BUFFSIZE; /* temp buffer size */

    if (bzerror (NULL, BZ2_bzDecompressInit (&zstream, 0, 0))) {
        free (zdata);
        return true;
    }    

    remove (pdest);
    /* open location where sava decompressed data */
    if (!(fd = fopen (pdest, "wb"))){
        fprintf (stderr, "\n"
        "Cannot decompress data to %s: %s\n", pdest, strerror (errno));
        BZ2_bzDecompressEnd (&zstream);
        free (zdata);
        return true;
    }

    rval = BZ_OK;
    while ((rval = BZ2_bzDecompress (&zstream)) == BZ_OK) {

        if (fwrite (buffer, 1, BUFFSIZE - zstream.avail_out, fd) !=
            BUFFSIZE - zstream.avail_out)
        {
            fprintf (stderr, "\n"
            "Cannot write decompress data to %s: %s\n", dest, strerror (errno));
            fclose (fd);
            BZ2_bzDecompressEnd (&zstream);
            free (zdata);
            return true;
        }

        /* data saved, we can reuse our buffer */
	    zstream.next_out  = buffer;
	    zstream.avail_out = BUFFSIZE; /* all BUFFSIZE bytes available :P */
	}

    free (zdata);
    if (dest == NULL)
     free (pdest);
    
	if (bzerror (zpath, rval)) {
	    BZ2_bzDecompressEnd (&zstream);
	    return true;
    }
    
    fwrite (buffer, 1, BUFFSIZE - zstream.avail_out, fd);
    fclose (fd);
    
    bzerror (NULL, BZ2_bzDecompressEnd (&zstream));

    return false;
}

/* return type of protocol of url saved in `str' */
protocol
check_proto_t (const char *str)
{
    if (!strncasecmp(str, "http:", 5))
	 return http;

    if (!strncasecmp(str, "ftp:", 4))
	 return ftp;

    _error (3, "\n"
    "Fatal error: repository \"", str,
	 "\" isn't HTTP or FTP.\n"
	 "Check your slackyd.conf.\n\n");

    return 0; /* :-m */
}

/* make update data directories */
static void
update_dir (void)
{
	const char *dbranch[BRANCHES] = { "/", "/patches", "/extra" };
	unsigned r = 0;
	char path[BUFFSIZE0];
	bool slackware = false;
	
	for (r = 0; r < REPOS[0].N; r++) {
		
	if (REPOS[r].SLACKWARE && slackware)
	 continue; /* we use only one official mirror */
		
	/* repository directory */
	snprintf (path, BUFFSIZE, "%s/%s", DATADIR, REPOS[r].name);
	xmkdir (path, 0777);
	
	if (!REPOS[r].SLACKWARE)
	 continue; /* extra repository haven't patches and extra branches */
	
	/* repository patches directory */
	snprintf (path, BUFFSIZE, "%s/%s/%s", DATADIR, REPOS[r].name, dbranch[1]);
	xmkdir (path, 0777);
	/* repository extra directory */
	snprintf (path, BUFFSIZE, "%s/%s/%s", DATADIR, REPOS[r].name, dbranch[2]);
	xmkdir (path, 0777);
	
	if (REPOS[r].SLACKWARE)
	 slackware = true;
	}

	return;
}

/* retrieve a file list of REPOS[repository_index] */
static int
update_get_file (unsigned repository_index, branches branch, const char *file)
{
	unsigned i = repository_index;
	char src [BUFFSIZE0] = { 0 },
		 dest[BUFFSIZE0] = { 0 },
		 msg [BUFFSIZE0] = { 0 };
    bool timestamp = !opt.force;

    /* if we are downloading checksums list or manifest, file list or packages list was changed.
     * In this case we can skip timestamp check, otherwise file (checksum list or manifest) will
     * seem to be always update !
     */
    if (!strcmp (file, CHECKSUMS) ||
        !strcmp (file, ZMANIFEST))
    timestamp = false;
		
	/* make (source and destination) path of our file and a message */
	switch (branch) {
		case _PACKAGES:
			if (REPOS[i].SLACKWARE && strcmp (file, ZMANIFEST) == 0)
			 snprintf (src,  BUFFSIZE, "%s/slackware/%s", REPOS[i].path, file);
            else
             snprintf (src,  BUFFSIZE, "%s/%s", REPOS[i].path, file);
            snprintf (dest, BUFFSIZE, "%s/%s/%s", DATADIR, REPOS[i].name, file);
			snprintf (msg,  BUFFSIZE, " `- Downloading %s ", file);
			break;
		case _PATCHES:
			snprintf (src,  BUFFSIZE, "%s/patches/%s", REPOS[i].path, file);
			snprintf (dest, BUFFSIZE, "%s/%s/patches/%s", DATADIR, REPOS[i].name, file);
			snprintf (msg,  BUFFSIZE, " `- Downloading patches %s ", file);
			break;
		case _EXTRA:
			snprintf (src,  BUFFSIZE, "%s/extra/%s", REPOS[i].path, file);
			snprintf (dest, BUFFSIZE, "%s/%s/extra/%s", DATADIR, REPOS[i].name, file);
			snprintf (msg,  BUFFSIZE, " `- Downloading extra %s ", file);
			break;
		default:
			slassert (NULL);
			break;
	
	}
		
	if (REPOS[i].proto_t == http)
	 return get_file_over_http (msg, REPOS[i].hostname, src, dest, true, true, timestamp);
	else
	if (REPOS[i].proto_t == ftp)
	 return get_file_over_ftp  (msg, REPOS[i].hostname, src, dest, true, true, timestamp);

	slassert (NULL); /* error */
	return 0;
}

/* uncompress MANIFEST.bz2 of repository `i' branch `b' */
static void
explode_manifest (unsigned i, branches b)
{
    const char *sbranches[BRANCHES] = { "patches", "\b", "extra" };
    
    fprintf (stdout, " >> Uncompressing %s %s: ", sbranches[b], ZMANIFEST);
    fflush (stdout);

	if (!bz_decompress (make_rpath (i, b, ZMANIFEST), NULL))
	 fprintf (stdout, "done.\n");

    return;
}

/* File downloaded or already updated... */
static bool
down_ok (int rval)
{
    return (rval == FILE_DOWNLOADED || rval == FILE_ALREADY_UPDATE);
}

/* update repository file list and checksum list */
int
update (void)
{
    #define NFILES 3

    branches official_branches[BRANCHES] = { _PACKAGES, _PATCHES, _EXTRA };
    const char *official_files[NFILES]    = { PACKAGES, CHECKSUMS, ZMANIFEST };
    unsigned r = 0, fdown = 0, fall = 10, i, j;
    bool slackware = false, tmpopt;
    int rval;
    
    tmpopt = opt.force;
    if (packages_list_check (false, false) > 0)
    {
        if (opt.warn)
         fprintf (stderr, "*** Warning: broken packages list, forcing update.\n\n");
        opt.force = true;
    }

    update_dir ();

    /* Update official slackware files list */
    while (r < REPOS[0].N && REPOS[r].SLACKWARE && !slackware && fdown != fall)
    {
        fprintf (stdout, "Repository: %s [%s]\n", REPOS[r].name, REPOS[r].hostname);

        /* First of all update FILELIST.TXT;
         * If it is already update, we assume that all is update. Else
         * if it cannot be downloaded we assume that repository is not standard.
         */
        if ((rval  = update_get_file (r, _PACKAGES, FILELIST)) != FILE_DOWNLOADED)
        {
            break;
        }
        
        fdown = 1; /* FILELIST.TXT was retreived. */
        
        /* For each branch, /, /patches and /extra ... */
        for (i = 0; down_ok (rval) && i < BRANCHES; i++)
        {
            /* For each file, PACKAGES.TXT, CHECKSUMS.TXT and MANIFEST.bz2 ... */
            for (j = 0; down_ok (rval) && j < NFILES; j++)
            {
                rval = update_get_file (r, official_branches[i], official_files[j]);

                /* If we are downloading PACKAGES.TXT and if it is already update,
                 * we assume that CHECKSUMS.TXT and MANIFEST.bz2 *OF SAME BRANCH* is
                 * also updated.
                 */
                if (!strcmp (official_files[j], PACKAGES)
                 && rval == FILE_ALREADY_UPDATE)
                {
                    fdown += 3;
                    break;
                }

                /* A MANIFEST.bz2 maybe retreived; uncompress it. */
                if (down_ok (rval) && !strcmp (official_files[j], ZMANIFEST))
                {
                    explode_manifest (r, official_branches[i]);
                }

                /* A file was downloaded or is already update */
                if (down_ok (rval)) fdown++;
            }
        }

        r++;
        slackware = true;
        
        if (fdown != fall)
        {
            fprintf (stderr, "\nWarning: cannot retreive all file (%u/%u). Trying next: %s\n\n",
                fdown, fall, (r < REPOS[0].N && REPOS[r].SLACKWARE) ? "found" : "nothing");
            slackware = false;
        }
    }

    /* Update extra repository files list */
    while (r < REPOS[0].N)
    {
        if (REPOS[r].SLACKWARE)
        {
            r++;
            continue;
        }
         
        fprintf (stdout, "Repository: %s [%s]\n", REPOS[r].name, REPOS[r].hostname);

        if (update_get_file (r, _PACKAGES, FILELIST)  == FILE_DOWNLOADED
         && update_get_file (r, _PACKAGES, PACKAGES)  == FILE_DOWNLOADED
         && update_get_file (r, _PACKAGES, CHECKSUMS) == FILE_DOWNLOADED
         && update_get_file (r, _PACKAGES, ZMANIFEST) == FILE_DOWNLOADED)
        {
            explode_manifest (r, _PACKAGES);
        }

        r++;
    }
        
    opt.force = tmpopt;
        
  
	return EXIT_SUCCESS;
}

	
/* check for new version available on upgrade, but already installed */
static bool
pkg_u_installed (const char *pkg)
{
    char buffer[BUFFSIZE0] = { 0 }, *p;
    struct stat s;

    snprintf (buffer, BUFFSIZE, "%s/%s", PKGDIR, pkg);
    if (!(p = strrchr (buffer, '.')))
     return false;
    else
     *p = 0x00;
    
    return (!stat (buffer, &s)) ? true : false;
}

static int
search_new (const char *pkgname, upgrade_t *toupgrade[])
{
	unsigned i, j, pnew = 0, sub_new = 0, checked = 0;
    int rval = 0, diff[2], sflags, cflags;
    regex_t preg;
    pkg_t **packages = NULL;
    unsigned npackages = 0x00;
        
       
    fprintf (stdout, "Reading packages data: ");
    fflush (stdout);

    sflags = (pkgname && opt.use_regex) ? MATCH_USING_REGEX : MATCH_EXACTLY_NAME_INST;
    rval = search (sflags, pkgname, &packages, &npackages);  
    if (rval == EXIT_FAILURE) {
         if (!pkgname)
          fprintf (stdout, "\n\n"
          "No packages found !?\n"
          "Try to update packages list.\n");
         else
          fprintf (stdout, "nothing to do for `%s'.\n", pkgname);
          
         return EXIT_FAILURE;
    }
 
    fprintf (stdout, "done.\n");

    if (!opt.verbose) {
        fprintf (stdout, "Searching new packages: ");
        fflush (stdout);
    }
    else
    putchar ('\n');
    
    
    cflags = REG_EXTENDED | REG_NOSUB;
    if (opt.use_regex && pkgname) {
        if (opt.case_insensitive)
         cflags |= REG_ICASE;
        xregcomp (&preg, pkgname, cflags);
    }

    for (i = 0; i < nginst; i++) {

    if ((pkgname && opt.use_regex == true  && regexec (&preg, ginst[i].name, 0, NULL, 0))
     || (pkgname && opt.use_regex == false && !xstrstr (ginst[i].name, pkgname)))
     {
         continue;
     }
     
    if (opt.verbose) {
        fprintf (stdout, "Verifying %s: ", ginst[i].name);
        fflush (stdout);
    }
    
    checked++;
    sub_new  = 0;
    
    for (j = 0; j < npackages; j++) {

        /* if current available package haven't same name of current
         * package installed, of if already installed, go to begin for()
         */
        if (strcmp (packages[j]->pstruct.name, ginst[i].pstruct.name)
        ||  pkg_u_installed (packages[j]->name))
         continue;

        diff[0] = strverscmp (ginst[i].pstruct.version, packages[j]->pstruct.version);
	    diff[1] = atoi (ginst[i].pstruct.build) - atoi (packages[j]->pstruct.build);
               
        if (diff[0] < 0
        || (!diff[0] && diff[1] < 0)
        || (!diff[0] && !diff[1] && packages[j]->branch == _PATCHES))
        {
            /* found new available package with version and/or build major to local package */
            
            *toupgrade = xrealloc (*toupgrade, (pnew + 1) * sizeof (upgrade_t));
            /* skip memory leak :P */
            if (sub_new == 0)
            {
                (*toupgrade)[pnew].remote = NULL;
                (*toupgrade)[pnew].local  = xstrdup (ginst[i].name);
            }
            add_pkg_t (&(*toupgrade)[pnew].remote, &sub_new, packages[j]);
        }
    } /* end available packages for() */
    
    if (sub_new > 0) {
       	(*toupgrade)[pnew].sub_new = sub_new;
       	pnew++;
       	if (opt.verbose)
       	{
       	    fprintf (stdout, "found %u new.\n", sub_new);
        }
    }
    else
    if (opt.verbose)
    {
    	fprintf (stdout, "nothing to do.\n");
    }

	} /* end installed packages list for() */

    
    if (opt.use_regex && pkgname)
     regfree (&preg);
    free_pkgs_t (&packages, npackages);
    
    if (!opt.verbose)
     fprintf (stdout, "done !\n");

    
    if (checked == 0) {
        fprintf (stdout, "\n"
        "There aren't packages \"%s\" installed.\n", pkgname);
        return EXIT_FAILURE;
    }
        
    putchar ('\n');

    if (pnew == 0)
    {
        if (pkgname)
        {
            fprintf (stdout, "All packages \"%s\" are updated.\n", pkgname);
        }
        else
        {
            fprintf (stdout, "All packages are updated.\n");
        }
    }

	return (pnew > 0) ? (int) pnew : EXIT_FAILURE;
}


/* upgrade packages installed */
int
upgrade (const char *pkgname)
{
    pkg_t **gpkg = NULL;
    int i, pnew, iflags;
    unsigned j, ngpkg = 0;
    upgrade_t *toupgrade = NULL;
    char c = 0;
    
    pkg_t **prequired = NULL;
    unsigned nprequired = 0;
    bool ch;

    packages_list_check (true, true);

    iflags = (pkgname && opt.use_regex) ? MATCH_USING_REGEX : MATCH_ALL_NAME;
    if (pkgname && is_installed (pkgname, iflags, 0) == PACKAGE_STATUS_NOT_INSTALLED) {
        fprintf (stderr,
        "No packages \"%s\" are installed.\n", pkgname);
        return EXIT_FAILURE;
    }
    

    if ((pnew = search_new (pkgname, &toupgrade)) < 1) {
        return EXIT_FAILURE;
    }

    fprintf (stdout,
    "Found %d package%s to upgrade:\n"
    "----------------------------- \n", pnew, (pnew > 1) ? "s" : "");

    for (i = 0; i < pnew; i++) {
        printf ("%s:\n", toupgrade[i].local);
        fflush (stdout);

        for (j = 0; j < toupgrade[i].sub_new - 1; j++) {
            printf ("\t%s %s\n",
            toupgrade[i].remote[j]->name, pkg_type (toupgrade[i].remote[j]));
        }
        printf ("\t%s %s\n",
        toupgrade[i].remote[j]->name, pkg_type (toupgrade[i].remote[j]));    
	}

    putchar ('\n');

    /* choice package to upgrade */
    for (i = 0; i < pnew; i++) {

    /* set `ch' to false; we use this variable to track package choice */
    ch = false;

    sort_pkg (&toupgrade[i].remote, &toupgrade[i].sub_new);

    if (toupgrade[i].sub_new > 1) {
        fprintf (stdout, "Found %u different new version of package %s:\n",
        toupgrade[i].sub_new, toupgrade[i].remote[0]->pstruct.name);
        for (j = 0; j < toupgrade[i].sub_new; j++)
         fprintf (stdout, "  --> %s %s\n",
         toupgrade[i].remote[j]->name, pkg_type (toupgrade[i].remote[j]));
    }

    for (j = 0; j < toupgrade[i].sub_new; j++) {

        if (!opt.force) {
            if (j == 0)
             fprintf (stdout, "Upgrade %s to:\n", toupgrade[i].local);

            fprintf (stdout, "\t%s %s ? [y/N] ", toupgrade[i].remote[j]->name,
            (toupgrade[i].sub_new == 1) ? pkg_type (toupgrade[i].remote[j]) : "\b");
        }
        else
         if (toupgrade[i].sub_new > 1)
         fprintf (stdout, "Will be used %s !\n", toupgrade[i].remote[0]->name);

        fflush (stdout);

        if (opt.force == true || (c = getchar()) == 'Y' || c == 'y') {

            ch = true;
            ngpkg++;

            gpkg = (pkg_t **)
                xrealloc (gpkg, ngpkg * sizeof (pkg_t *));
            gpkg[ngpkg - 1] = (pkg_t *)
                xmalloc (sizeof (pkg_t));

            cp_pkg (toupgrade[i].remote[j], &gpkg[ngpkg - 1]);

            if (!opt.force)
             clrbuff();
            break;
        }
        if (c != '\n')
         clrbuff();
        }

        free (toupgrade[i].local);
        free_pkgs_t (&toupgrade[i].remote, toupgrade[i].sub_new);

        if (ch && opt.check_dep) {

            search_mreq (&gpkg[ngpkg - 1], 0);
            
	        /* search missing packages dependencies in repository. If found ask if download. */
	        if (opt.check_dep && opt.resolve_dep && gpkg[ngpkg - 1]->nmrequired > 0)
	        {
	            choice_dep (&prequired, &nprequired, gpkg[ngpkg - 1], gpkg, ngpkg);
            }
	}
	}

	/* Free local missing packages list */
    choice_dep (NULL, NULL, NULL, 0, 0);
	
    free (toupgrade);

    if (!opt.force)
     putchar ('\n');
    
    /* download packages and packages required */
    for (j = 0; j < nprequired; j++) {
        if (get_pkg (prequired[j]) == EXIT_SUCCESS)
         verify_md5 (prequired[j]);
     }
  
    free_pkgs_t (&prequired, nprequired);
    
    if (ngpkg == 0)
      fprintf (stdout, "Nothing was choiced.\n");
    else
      for (j = 0; j < ngpkg; j++) {
        if (get_pkg (gpkg[j]) == EXIT_SUCCESS)
         verify_md5 (gpkg[j]);
       }

    free_pkgs_t (&gpkg, ngpkg);
    
    return EXIT_SUCCESS;

}
