# vim:set fileencoding=utf-8 et ts=4 sts=4 sw=4:
#
#   apt-listchanges - Show changelog entries between the installed versions
#                     of a set of packages and the versions contained in
#                     corresponding .deb files
#
#   Copyright (C) 2000-2006  Matt Zimmerman  <mdz@debian.org>
#   Copyright (C) 2006       Pierre Habouzit <madcoder@debian.org>
#   Copyright (C) 2016       Robert Luberda  <robert@debian.org>
#
#   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 St, Fifth Floor, Boston, MA 02110-1301, USA.
#

import os

import ALCLog
from ALChacks import _

def _parse_apt_bool(value):
    # Based on StringToBool() from apt-pkg/contrib/strutl.cc in apt source
    # and should return same result as StringToBool(value, false)
    return value.lower() in [ '1', 'yes', 'true', 'with', 'on', 'enable' ]

def _parse_apt_int(value):
    # This function should match Configuration::FindI() from apt's
    # apt-pkg/contrib/configuration.cc, except for values like '1something'
    try:
        return int(value)
    except Exception:
        return 0

class AptPipelineError(Exception):
    pass

class AptPipeline(object):
    def __init__(self, config):
        super().__init__()
        self._config = config

    def read(self):
        if self._config.debug:
            ALCLog.debug(_("APT pipeline messages:"))

        with self._open_apt_fd() as fd:
            self._read_version(fd)
            self._read_options(fd)
            debs = self._read_packages(fd)

        if self._config.debug:
            ALCLog.debug(_("Packages list:"))
            for d in debs:
                ALCLog.debug("\t%s" % d)
            ALCLog.debug("")
        return debs

    def _open_apt_fd(self):
        if 'APT_HOOK_INFO_FD' not in os.environ:
            raise AptPipelineError(_("APT_HOOK_INFO_FD environment variable is not defined\n"
                    "(is Dpkg::Tools::Options::/usr/bin/apt-listchanges::InfoFD set to 20?)"))

        try:
            apt_hook_info_fd_val = int(os.environ['APT_HOOK_INFO_FD'])
        except Exception as ex:
            raise AptPipelineError(_("Invalid (non-numeric) value of APT_HOOK_INFO_FD"
                                     " environment variable")) from ex
        if self._config.debug:
            ALCLog.debug(_("Will read apt pipeline messages from file descriptor %d") % apt_hook_info_fd_val)

        if apt_hook_info_fd_val == 0: # TODO: remove this backward compatibility code in Debian 10 (Buster)
            ALCLog.warning(_("Incorrect value (0) of APT_HOOK_INFO_FD environment variable.\n"
                             "If the warning persists after restart of the package manager (e.g. aptitude),\n"
                             "please check if the /etc/apt/apt.conf.d/20listchanges file was properly updated."))

        elif apt_hook_info_fd_val < 3:
            raise AptPipelineError(_("APT_HOOK_INFO_FD environment variable is incorrectly defined\n"
                    "(Dpkg::Tools::Options::/usr/bin/apt-listchanges::InfoFD should be greater than 2)."))

        try:
            return os.fdopen(apt_hook_info_fd_val, 'rt')
        except Exception as ex:
            raise AptPipelineError(_("Cannot read from file descriptor %(fd)d: %(errmsg)s")
                                  % {'fd': apt_hook_info_fd_val, 'errmsg': str(ex) }) from ex

    def _read_version(self, fd):
        version = fd.readline().rstrip()
        if version != "VERSION 2":
            raise AptPipelineError(_("Wrong or missing VERSION from apt pipeline\n"
                "(is Dpkg::Tools::Options::/usr/bin/apt-listchanges::Version set to 2?)"))
        if self._config.debug:
            ALCLog.debug("\t%s" % version)

    def _read_options(self, fd):
        while True:
            line = fd.readline().rstrip()
            if self._config.debug:
                ALCLog.debug("\t%s" % line)
            if not line:
                return

            if (not self._config.ignore_apt_assume and
                  line.startswith('APT::Get::Assume-Yes=') and
                  _parse_apt_bool(line[len('APT::Get::Assume-Yes='):]) ):
                self._config.confirm = False
                # Set self._config.quiet as well to force non-interactive frontend
                self._config.quiet = max(1, self._config.quiet)
            elif line.startswith('quiet='):
                self._config.quiet = max(_parse_apt_int(line[len('quiet='):]), self._config.quiet)

    def _read_packages(self, fd):
        filenames = {}
        toconfig = []
        toremove = []
        hasupgrade = False

        for pkgline in fd.readlines():
            pkgline = pkgline.rstrip()
            if self._config.debug:
                ALCLog.debug("\t%s" % pkgline)
            if not pkgline:
                break

            (pkgname, oldversion, compare, newversion, filename) = pkgline.split(None, 4)
            if compare != '<': # ignore downgrades or re-installations
                continue

            if filename == '**REMOVE**' or filename == '**ERROR**':
                toremove.append(pkgname)
                continue

            # New installs (oldversion equal to '-') are not ignored to support
            # a case when changelog is moved from one package to a dependent
            # package built from the same source (see p7zip-full 15.09+dfsg-3).
            if oldversion != '-':
                hasupgrade = True

            if filename == '**CONFIGURE**':
                toconfig.append(pkgname)
            else:
                filenames[pkgname] = filename

        # Quit early if no package has been upgraded (e.g. only new installs or removals)
        if not hasupgrade:
            return []

        # Sort by configuration order.  THIS IS IMPORTANT.  Sometimes, a
        # situation exists where package X contains changelog.gz (upstream
        # changelog) and depends on package Y which contains
        # changelog.Debian.gz (Debian changelog).  Until we have a more
        # reliable method for determining whether a package is Debian
        # native, this allows things to work, since Y will always be
        # configured first.

        # apt doesn't explicitly configure everything anymore, so sort
        # the things to be configured first, and then do everything else
        # in alphabetical order.  Also, drop from the list everything
        # that's to be removed.
        for pkg in toremove:
            if pkg in filenames:
                del filenames[pkg]

        ordered_filenames = []
        for pkg in toconfig:
            if pkg in filenames:
                ordered_filenames.append(filenames[pkg])
                del filenames[pkg]

        ordered_filenames.extend(sorted(filenames.values()))
        return ordered_filenames
