#!/usr/bin/python

import argparse
import functools
import sys
import os
import re
import time

os.environ['ETP_GETTEXT_DOMAIN'] = "entropy-server"

# Entropy imports
sys.path.insert(0, '/usr/lib/entropy/lib')
sys.path.insert(0, '/usr/lib/entropy/server')
sys.path.insert(0, '/usr/lib/entropy/client')
sys.path.insert(0, '../lib')
sys.path.insert(0, '../server')
sys.path.insert(0, '../client')


# Entropy imports
from entropy.output import print_info, print_error, print_warning, \
    print_generic

# Portage imports
os.environ['ACCEPT_PROPERTIES'] = "* -interactive"
os.environ['FEATURES'] = "split-log"
os.environ['CMAKE_NO_COLOR'] = "yes"

from _emerge.actions import load_emerge_config
import portage
import portage.dep
import portage.versions


class AntiMatterPackage(object):
    """
    This object describes a potentially available update and
    exposes an API for easy retrieval of the update information.
    """

    def __init__(self, vardb, portdb, installed, available, status):
        """
        Constructor.

        @param vardb: Portage vardb object.
        @type vardb: opaque
        @param portdb: Portage portdb object.
        @type portdb: opaque
        @param installed: Atom object of the installed package.
        @type installed: portage.dep.Atom
        @param available: Atom object of the available package.
        @type available: portage.dep.Atom
        @param status: 1 for upgrade, -1 for downgrade, 0 for rebuild
        @type status: int
        """
        self._vardb = vardb
        self._portdb = portdb
        self._installed = installed
        self._available = available
        self._status = status

    def slot(self):
        """
        Return the package slot.
        """
        return self._installed.slot

    def keyslot(self):
        """
        Return the package key:slot string.
        """
        target = self.target()
        return "%s:%s" % (target.cp, target.slot)

    def installed(self):
        """
        Return the installed package Atom object.
        """
        return self._installed

    def available(self):
        """
        Return the available package Atom object or None.
        """
        return self._available

    def target(self):
        """
        Automatically return the right object between available()
        and installed(). In particular, if available() is None,
        installed() is returned.
        """
        if self._available:
            return self._available
        return self._installed

    def dropped_upstream(self):
        """
        Return whether the package has been dropped upstream
        and no more updated versions are available for update.
        """
        return self._available is None

    def upgrade(self):
        """
        Return whether the package would be upgraded.
        """
        return self._status == 1

    def downgrade(self):
        """
        Return whether the package would be downgraded.
        """
        return self._status == -1


class BaseAntiMatterResult(object):
    """
    Base class for implementing AntiMatter result
    objects. Subclasses must implement the notify()
    method.
    """

    def __init__(self, result, quiet, verbose):
        """
        Constructor.

        @param result: list of AntiMatterPackage objects
        @type result: list
        @param quiet: quiet output enabled (print one item per line)
        @type quiet: bool
        @param verbose: verbose output enabled (print the full version)
        @type verbose: bool
        """
        self._result = result
        self._quiet = quiet
        self._verbose = verbose

    def notify(self):
        """
        Notify the result of an AntiMatter run using
        the list of AntiMatterPackage objects passed
        to the constructor.
        """
        raise NotImplementedError()


class StdoutAntiMatterResult(BaseAntiMatterResult):
    """
    BaseAntiMatterResult subclass that prints the results
    to stdout.
    """

    def __init__(self, result, quiet, verbose):
        """
        Constructor.

        @param result: list of AntiMatterPackage objects
        @type result: list
        @param quiet: quiet output enabled
        @type quiet: bool
        @param verbose: verbose output enabled
        @type verbose: bool
        """
        super(StdoutAntiMatterResult, self).__init__(
            result, quiet, verbose)

    def notify(self):
        """
        Overridden from BaseAntiMatterResult
        """
        for package in self._result:
            if self._verbose:
                target = package.target()
            else:
                target = package.keyslot()

            if self._quiet:
                print_generic(target)
            else:
                print_info(target)


class AntiMatter(object):
    """
    AntiMatter is a package update scanner that uses
    Portage and Entropy to determine
    """

    def __init__(self, nsargs):
        """
        Constructor.

        @param nsargs: argparse's parsed arguments.
        @param entropy_obj: an Entropy instance or None
        """
        self._nsargs = nsargs

    def _get_dbs(self):
        """
        Return a tuple containing (vardb, portdb)
        """
        emerge_config = load_emerge_config()
        emerge_settings, emerge_trees, _mtimedb = emerge_config
        settings = portage.config(clone=emerge_settings)

        portdb = emerge_trees[settings["ROOT"]]["porttree"].dbapi
        if not portdb.frozen:
            portdb.freeze()
        vardb = emerge_trees[settings["ROOT"]]["vartree"].dbapi

        return vardb, portdb

    def _new_scan(self):
        """
        Internal scan method, executes the actual scan and retuns
        a raw list of AntiMatterPackage objects.
        """
        vardb, portdb = self._get_dbs()
        new_days_old_secs = self._nsargs.new_days_old * 3600 * 24
        result = []

        cp_all = portdb.cp_all()
        cp_all.sort()
        root = portdb.porttree_root
        for count, package in enumerate(cp_all):

            count_str = "[%s of %s]" % (
                count, len(cp_all),)

            if self._nsargs.verbose:
                print_warning("%s :: %s" % (count_str, package),
                              back=True)

            cp_dir = os.path.join(root, package)
            try:
                mtime = os.path.getmtime(cp_dir)
            except (OSError, IOError):
                mtime = 0.0

            if abs(time.time() - mtime) >= new_days_old_secs:
                # not new enough
                continue

            best_installed = portage.best(vardb.match(package))
            if best_installed:
                # package key is already installed, ignore
                continue

            best_visible = portage.best(portdb.match(package))
            if not best_visible:
                # wtf? package masked?
                continue

            try:
                slot, repo = portdb.aux_get(
                    best_visible, ["SLOT", "repository"])
            except KeyError:
                # portage is scrappy
                continue

            atom = portage.dep.Atom(
                "=%s:%s::%s" % (best_visible, slot, repo),
                allow_wildcard=True,
                allow_repo=True)

            pkg = AntiMatterPackage(
                vardb, portdb, None, atom, 1)
            result.append(pkg)

        if cp_all and self._nsargs.verbose:
            print_generic("")

        return result

    def _scan(self):
        """
        Internal scan method, executes the actual scan and retuns
        a raw list of AntiMatterPackage objects.
        """
        vardb, portdb = self._get_dbs()
        result = []

        cpv_all = vardb.cpv_all()
        cpv_all.sort()
        for count, package in enumerate(cpv_all):

            count_str = "[%s of %s]" % (
                count, len(cpv_all),)

            slot, repo = vardb.aux_get(package, ["SLOT", "repository"])

            atom = portage.dep.Atom(
                "=%s:%s::%s" % (package, slot, repo),
                allow_wildcard=True,
                allow_repo=True)

            if self._nsargs.verbose:
                print_warning("%s :: %s" % (count_str, atom),
                              back=True)

            key_slot = "%s:%s" % (atom.cp, atom.slot)

            best_visible = portage.best(vardb.match(key_slot))

            if not best_visible:
                # dropped upstream
                pkg = AntiMatterPackage(
                    vardb, portdb, atom, None, -1)
                result.append(pkg)
                if self._nsargs.verbose:
                    print_error(
                        "  %s no longer upstream or masked" % (key_slot,))
                continue

            cmp_res = portage.versions.pkgcmp(
                portage.versions.pkgsplit(best_visible),
                portage.versions.pkgsplit(package))

            pkg = AntiMatterPackage(
                vardb, portdb, atom,
                best_visible, cmp_res)
            result.append(pkg)

        if cpv_all and self._nsargs.verbose:
            print_generic("")

        return result

    def scan(self):
        """
        Execute a scan of the system and return a BaseAntiMatterResult
        object.
        """
        if self._nsargs.new:
            result = self._new_scan()
        else:
            result = self._scan()
        # apply filtering basing on arguments
        if self._nsargs.extinguished:
            result = [x for x in result if x.dropped_upstream()]
        elif self._nsargs.upgrade:
            result = [x for x in result if x.upgrade()]
        elif self._nsargs.downgrade:
            result = [x for x in result if x.downgrade()]

        def _regex_filter(regex, x):
            target = x.target()
            operator = target.operator
            target_str = target[len(operator):]
            return regex.match(target_str)

        for regex in self._nsargs.filters:
            result = list(filter(
                    functools.partial(_regex_filter, regex),
                    result))

        return StdoutAntiMatterResult(
            result, self._nsargs.quiet, self._nsargs.verbose)


if __name__ == "__main__":

    parser = argparse.ArgumentParser(
        description='Automated package updates scanner',
        formatter_class=argparse.RawDescriptionHelpFormatter)

    parser.add_argument("--verbose", "-v", action="store_true",
                        default=False, help="verbose output")
    parser.add_argument("--quiet", "-q", action="store_true",
                        default=False, help="quiet output")

    parser.add_argument("--filter", "-f", dest="filters",
                        action="append", default=[],
                        help="filter package atoms using the given "
                        "regular expressions. They will be applied in "
                        "the given order, against the full package atom "
                        "string (which includes version, slot, repo: "
                        " app-foo/bar-1.2.3:slot::repo).")

    mg_group = parser.add_mutually_exclusive_group(required=True)
    mg_group.add_argument("--extinguished", "-e", action="store_true",
                          default=False,
                          help="list dead packages only (those which "
                          "upstream dropped)")
    mg_group.add_argument("--upgrade", "-u", action="store_true",
                          default=True,
                          help="list packages that would be upgraded "
                          "(default action)")
    mg_group.add_argument("--downgrade", "-d", action="store_true",
                          default=False,
                          help="list packages that would be downgraded")
    mg_group.add_argument("--new", "-n", action="store_true",
                          default=False,
                          help="list packages that have been recently "
                          "added")

    nsargs = None
    try:
        nsargs = parser.parse_args(sys.argv[1:])
        nsargs.filters = [re.compile(x) for x in nsargs.filters]
        # still hardcoded, but candidate for argparse argument
        # used in the calculation of new packages.
        nsargs.new_days_old = 14
    except re.error as err:
        print_error("Error, invalid regexp: %s" % (err,))
        raise SystemExit(1)

    antimatter = AntiMatter(nsargs)
    antimatter.scan().notify()

    raise SystemExit(0)
