#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#
# pukka.py
#
# Script to find missing dependencies in current based on stable
#
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++#
#                                                                             #
# pukka - a script to find subdependencies                              #
#                                                                             #
# Copyright Tim Beech <tim~beech~at~gmail~dot~com>.                      #
#                                                                             #
# Partly based on Frédéric Galusik's code from pkgtxt2db
# and a script from SaLT by Cyrille Pontvieux.

# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. #
#                                                                             #
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++#
import os
import sys
import urllib2
import gzip
import argparse
import string
from ParsePkgtxt import Package
from simpleconfig import SimpleConfig
import cPickle as pickle

# Parse the CLI options
parser = argparse.ArgumentParser(
        prog='pukka',
        description='Download and interrogate PACKAGES.TXTs',
        epilog=
        'e.g. pukka -p midori, check for midori and its dependencies without refreshing package data')

parser.add_argument('-u', '--update', action="store_true",
        default=False,
        help='Download fresh PACKAGES.TXT files')

parser.add_argument('-P', '--packager', action="store",
        dest='packager', default='',
        help='Search for packages by a particular packager')

parser.add_argument('-p', '--package', action="store",
        dest='package', default='',
        help = 'Detailed package status info')

parser.add_argument('-a', '--arch', action="store",
        dest='arch', default=os.uname()[4],
        help = 'Specify repo, defaults to local arch')

parser.add_argument('-d', '--download', action="store",
        dest='download', default='',
        help = 'Download package source and previous package')

parser.add_argument('-ns', '--nosource', action="store_true",
        default=False,
        help='Do not download package source files')

parser.add_argument('-np', '--nopackage', action="store_true",
        default=False,
        help='Do not download package files (just get source)')

parser.add_argument('-k', '--slkbuildonly', action="store_true",
        default=False,
        help='When downloading source files, just get SLKBUILD')

parser.add_argument('-D', '--depcheck', action="store",
        dest='Package', default='',
        help='Check unpackaged package dependencies')

parser.add_argument('-s', '--stable', action="store",
        dest='stable', default='',
        help = 'Specify stable release, default 14.0')

parser.add_argument('-c', '--current', action="store",
        dest='current', default='',
        help = 'Specify release for current, default 14.1')

parser.add_argument('-C', '--dlcurrent', action="store_true",
        default=False,
        help='Download source or package files from current repo')

#parser.add_argument('-S', '--dlslack', action="store_true",
#        default=False,
#        help='Download source or package files from Slackware repo')

parser.add_argument('-m', '--mirror', action="store",
        dest='mirror', default='',
        help = 'Choose a different mirror (default http://download.salixos.org)')

args = parser.parse_args()

#vars
update = args.update
packager = args.packager
pkg = args.package
download = args.download
download_source = not args.nosource
download_package = not args.nopackage
slkbuild_only = args.slkbuildonly
download_current = args.dlcurrent
#download_slack = args.dlslack
depcheck= args.Package
if depcheck:
    pkg = depcheck
slash = '/'

#repo index vars
slxstable, slxcurrent, slkstable, slkcurrent = 0, 1, 2, 3

#dirs, paths to scripts
scriptdir = '/usr/libexec/pukka/'
rcdir = '/etc/pukka/'
wd = os.getcwd() + slash # we were called from here
configdir = os.path.expanduser('~/.config/pukka/')
os.system('mkdir -p ' + configdir)
if not os.path.isfile(configdir + 'pukkarc'):
    os.system('cp ' + rcdir + 'pukkarc ' + configdir)
slkparse = scriptdir + 'slkparse.sh'
slaptsearch = scriptdir + 'slapt-get-search.sh '
slaptupdate = scriptdir + 'slapt-get-update.sh '

#get or update default config values
configfile = SimpleConfig(configdir + 'pukkarc')

def chkConfig(config, default, newval):
    if newval:
        config.set(default, newval)
        print "The default for", default, "has been changed to", newval
        config.write()
        return newval
    else:
        return config.get(default)

stable = chkConfig(configfile, 'stable', args.stable)
current = chkConfig(configfile, 'current', args.current)

def wellformedURL(raw):
	"""
	Clean up URL. 
	wellformedURL(url)
	"""
	# a mere first stab
	return raw + slash

mirror = wellformedURL(chkConfig(configfile, 'mirror', args.mirror))

#arch
def inputArch(arg):
    """
    If the user input contains the string '64', choose that repo, otherwise i486.
    inputArch(arg)
    """
    if arg.find('64') != -1:
        return 'x86_64'
    else:
        return 'i486'
repo = inputArch(args.arch) #get i486 or x86_64

##### The following function listPackages is for the --packager option

def listPackages(nick):
    """
     List packages packaged by someone before but not yet this time
    listPackages(nick)
    """
    #open dictionaries
    dbStable = openDict('salix-', stable)
    dbCurrent = openDict('salix-', current)
    p = []
    if not download_current:
        for package in dbStable:
            pname = stripNum(dbStable[package][2])
            if pname == nick: # this is one he did
                if not elem(package,dbCurrent):
                    p.append(package)
        print "These packages were made by", nick, "for", stable, "but not yet for", current, ":"
    else: # just list what they've pacakged for current so far
        for package in dbCurrent:
            pname = stripNum(dbCurrent[package][2])
            if pname == nick: # this is one he did
                p.append(package)
        print "These packages have been made by", nick, "for", current, "so far:"
    p = sorted(p)
    for pkg in p:
        print pkg
    
##### The following function getFiles is for --download option

def getFiles(pkg, release):
    """
    Download source and package files for a given package
    from specified Salix repo; check first.
    getSource(pkg), version
    """
    version, parch, pkgrel, subdir = 0, 1, 2, 6
    target = 'salix-'
#    if download_slack:
#        target = 'slackware-'
    db = openDict(target, release)
    if elem(pkg, db):
        fields = db[pkg]
#        if not download_slack:
        if True:				#temporary fix while -S not implemented
            target = ''
        urlbase = mirror + repo + slash + target + release
        targetdir, apptype = splitPath(fields[subdir])
        if download_source: 
            srcbase = urlbase + '/source' + apptype + slash + pkg + slash
            downloadURL(wd, srcbase + 'SLKBUILD')
            if not slkbuild_only:
                os.system(slkparse + ' > ' + configdir + 'sourcelist')
                f = open(configdir + "sourcelist", "r")
                sources = f.read()
                f.close()
                clean(configdir, ['sourcelist']) 
                for filename in sources.split():
                    downloadURL(wd, srcbase + filename)
        if download_package:
            targetdir = targetdir +  apptype
            pkgbase = urlbase + targetdir + slash
            pkgnamebase = string.join([pkg, fields[version], fields[parch], fields[pkgrel]], '-')
            for suffix in ['.dep', '.md5', '.meta', '.txt', '.txz']:
                url = pkgbase + pkgnamebase+suffix
                downloadURL(wd, url)
    else:
        slaptSearch(pkg, release)


def splitPath(subdir):
    """
    Take '/salix/ap' (or the like) and return the two parts
    slackSalix(subdir)
    """
    target, apptype = subdir.split(slash)[1:]
    target, apptype = slash + target, slash + apptype
    return target, apptype






##### These functions correspond to the --package and --depcheck options

def overview(repos):
    """
    Returns a dictionary associating each package name with four
    boolean variables to indicate whether it is present in each repo
    
    """
    #create list of packages from all repos
    pkglist = []
    for repo in repos:
        for pkg in repo:
            pkglist.append(pkg)
    #remove duplicates
    pkglist = list(set(pkglist))

    #create dictionary from list using dictionary comprehension
    return {pkg:query(pkg, repos) for pkg in pkglist}

def query(pkg, repos):
    """
    Returns a list of boolean values for presence of single package
    per repo
    query(pkg, repos)
    """
    profile = []
    for repo in repos: 
        profile.append(elem (pkg, repo))
    return profile

def display(isPresent, repos):
    """
    Output messages about package status per repo
    I think all bases are covered - 2 ^ 3 = 8 options if not already in current
    Print version
    """
    versionfield = 0
    stableversion = ''
    if isPresent[slxstable]:
        stableversion = repos[slxstable][pkg][versionfield]

    if isPresent[slxcurrent]:
        # it has been packaged for Salix current
        currentversion = repos[slxcurrent][pkg][versionfield]
	packager = stripNum(repos[slxcurrent][pkg][2])
        print pkg + '-' + currentversion, "has already been packaged for", current, "by", packager
        if stableversion:
            print "The previous version was", stableversion
    elif not isPresent[slxstable]:
        # no Salix package this time or last, tell about Slack
        print pkg, "was not packaged for Salix", stable
        if isPresent[slkstable]:
            print "However, there was a package for it in Slackware", stable
            if isPresent[slkcurrent]:
                print "It has also been packaged for Slackware", current
            else:
                print "There does not appear to be a package for it in Slackware", current
        elif isPresent[slkcurrent]:
                print "Although it was not packaged for Slackware", stable, "either, there is a package for Slackware", current
        else:
            slaptSearch(pkg, stable)
    else:
        # it was packaged for Salix last time, tell about Slack
        print pkg, "has not been packaged yet. The previous version was:", pkg + '-' + stableversion
        if isPresent[slkcurrent]:
            print "However, there is a Slackware package for it."
            if not isPresent[slkstable]:
                print "There was no Slackware package for", stable
        elif isPresent[slkstable]:
            print "It was packaged for Slackware", stable, "but has not yet been packaged for", current

def listDeps(package, repos):
    """
    List unpackaged dependencies, recursively
    listDeps(package, repos)
    """
    deps = []
    depfield = 3
    s = repos[slxstable]
    c = repos[slxcurrent]
    S = repos[slkstable]
    C = repos[slkcurrent]
    stabledeps = s[package][depfield]
    for dep in stabledeps.split(","):
        if not elem(dep, c) and elem(dep,s):
            deps.append(dep)
            if not elem(dep, S) and not elem('|', dep):
                for subdep in listDeps(dep, repos):
                    deps.append(subdep)
    return sorted(list(set(deps))) # turn list to set to remove duplicates

#	#what depends on our package?
#	needs = []
#	for pkg in s:
#		if elem (package, s[pkg][depfield].split(",")):
#			needs.append(pkg)
#	if needs:
#		print "These packages depend on", package
#		for package in sorted(list(set(needs))):
#			print package
#		
#    return sorted(list(set(deps))) # turn list to set to remove duplicates
#

##### The following are general helper functions

def stripNum(pkgrel):
    """
    Strip digits from left recursively
    stripNum(pjgrel)
    """
    if pkgrel == '':
        return ''
    if pkgrel[0].isdigit():
        return stripNum(pkgrel[1:])
    else:
        return pkgrel

def downloadURL(location, url):
    """
    Download file from url and place it at local location
    downloadURL(location, URL)
    """
    try:
        f = urllib2.urlopen(url)
        print "Fetching ", url
        print ""
        # Open local file for writing
        with open(location + os.path.basename(url), "wb") as local_file:
            local_file.write(f.read())
    except urllib2.HTTPError, e:
        print "HTTP Error:", e.code, url
        return False
    except urllib2.URLError, e:
        print "URL Error:", e.reason, url
        return False

def clean(directory, files):
    """
    Remove any stray file or archive
    clean(directory, files)
    """
    for f in files:
        if os.path.isfile(directory + f):
            os.remove(directory + f)

def pickleNames(release):
    slaptdir = 'slapt-get-' + repo + '-' + release
    slxdict = 'salix' + slaptdir + '.' + 'pickle'
    slkdict = 'slackware' + slaptdir + '.' + 'pickle'
    return slxdict, slkdict


def repoToDict(release):
    """
    Convert package_data file to  dictionaries, save files, 
    return names; if present and not updating, just names
    """
    # returns a tuple for (salix, slackware)
    # these aren't the dictionaries, just their filenames
    slaptdir = 'slapt-get-' + repo + '-' + release
    slxdict, slkdict = pickleNames(release)
    slx = os.path.isfile(configdir + slxdict)
    slk = os.path.isfile(configdir + slkdict)
    if not (slx and slk): # if either pickle is missing, do both again
        pdata = configdir + slaptdir + slash + 'package_data'
        db = Package.parse(Package(), pdata) 
        salixdb, slackdb = {}, {} # initialise empty dicts
        pkgrelfield=2
        for pkg in db: # allocate to salix or slackware
            fields = db[pkg] # avoid consulting dict twice, while improving readability :)
            if stripNum(fields[pkgrelfield]): # if pkgrel has any letters, it's a Salix one
                salixdb.update({pkg:fields})
            else:
                slackdb.update({pkg:fields})
        with open(configdir + slxdict, 'wb') as handle:
            pickle.dump(salixdb, handle)
        with open(configdir + slkdict, 'wb') as handle:
            pickle.dump(slackdb, handle)
    return slxdict, slkdict # filenames, we have made sure they're really there

def parseRepos():
    """
    Open dict for each repo, return list of all four
    parseRepos()
    """
    repos = []
    for target in ['salix-', 'slackware-']:
        for release in [stable, current]:
            db = openDict(target, release)
            repos.append(db)
    return repos

def openDict(target, release):
    """
    Opens dictionary for either Salix or Slackware by 
    calling repoToDict, which returns both. 
    openDict(distro, release)
    """
    # This looks inefficient but actually isn't usually, because 
    # the dictionaries are only created if they don't exist.
    slxdict, slkdict = repoToDict(release)
    if target =='salix-':
        pdict = slxdict
    else:
        pdict = slkdict
    with open(configdir + pdict, 'rb') as handle:
        return pickle.load(handle)

def elem(item, listobj):
    """
    There must be a library function that does this
    elem(item, listobj)
    """
    for i in listobj:
        if i == item:
            return True
    return False

def slaptSearch(pkg, release):
    """
    Do slapt-get --search [pkg]
    slaptSearch(pkg, release)
    """
    print "There is no record of any such package for", release
    print "Doing slapt-get --search to look for related packages ..."
    command = [slaptsearch, configdir, repo, release, pkg]
    os.system(' '.join(command)) 


def slaptUpdate(repo, release):
    """
    Update slapt-get caches
    slaptUpdate()
    """
    args=slaptupdate + repo + ' ' + release + ' ' + mirror
    os.system(args)
    slxdict, slkdict = pickleNames(release)
    clean(configdir, [slxdict])
    clean(configdir, [slkdict])

###### main ...

def main():
    #download databases if absent or if update requested
    for release in [stable, current]:
        dirname = "slapt-get-" + repo + "-" + release
        if update or not os.path.isdir(configdir + dirname):    
            slaptUpdate(repo, release)

    #carry out specified actions
    if download:
        release = stable
        if download_current:
            release = current
        getFiles(download, release)
    if packager:
        listPackages(packager)
    if pkg: #first make dictionaries
        repos = parseRepos()
        if not depcheck: #just check one package
            display(query(pkg, repos), repos) #show detail
            if elem(pkg, repos[slxstable]):
                pkgrelfield = 2
                pkgrel = repos[slxstable][pkg][pkgrelfield]
                print pkg, "was packaged by", stripNum(pkgrel), "for Salix", stable
        else: #check for unpackaged dependencies
            pkgdict = overview(repos)
            if not elem(pkg,pkgdict):
                print "There is no sign of", pkg, "in", current
                slaptSearch(pkg, stable)
            elif pkgdict[pkg][slxstable]:
                deplist = listDeps(pkg, repos)
                pipe =[]
                for dep in deplist:
                    print dep
                    if elem('|', dep):
                        pipe.append(dep)
                if pipe:
                    print "Note that alternative dependencies (like jdk|jre) aren't processed further, but you can check them independently."
                if not deplist:
                    print pkg, "appears to have no unpackaged Salix dependencies."
            else:
                print pkg, "was not packaged for Salix", stable

if __name__ == '__main__':
    main()
