# 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>
#
#   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 configparser
import getopt
from glob import glob
import os
import re
import sys

import apt_pkg

from apt_listchanges import ALCLog
from apt_listchanges.ALChacks import _


class ALCConfig:
    def __init__(self):
        self.parser = configparser.ConfigParser()
        ### Command line only options
        self.apt_mode = False   # "--apt" option
        self.dump_seen = False
        self.profile = None

        ### Options that can be set in the config file
        self.frontend = 'pager'
        self.email_address = None
        self.email_format = 'text'
        self.verbose = False
        self.show_all = False
        self.confirm = False
        self.headers = False
        self.debug = False
        self.save_seen = None
        self.which = 'both'
        self.since = None
        self.latest = None
        self.reverse = False
        self.no_network = False
        self.select_frontend = False
        self.ignore_apt_assume = False
        self.ignore_debian_frontend = False
        self.titled = True
        self.hide = False
        self.log = '/var/log/apt/listchanges.log'
        self.filter = None
        self.capture_snapshots = None
        self.snapshot_dir = None

        ### Internal options for managing the config file options
        # - options that can be also used for command line parameters
        #   (after replacing '_' with '-', and adding trailing '=' if needed)
        self._bool_opts = [
            'confirm',
            'debug',
            'show_all',
            'headers',
            'verbose',
            'reverse',
            'dump_seen',
            'select_frontend',
            'no_network',
            'ignore_apt_assume',
            'ignore_debian_frontend',
            'titled',
            'hide',
        ]
        self._value_opts = [
            'frontend',
            'email_address',
            'email_format',
            'latest',
            'log',
            'filter',
            'save_seen',
            'since',
            'which',
        ]
        # - options that can be given only in the config file
        self._cfgfile_only_opts = [
            'browser',
            'pager',
            'xterm',
            'capture_snapshots',
            'snapshot_dir',
        ]
        self._cfgfile_converters = {
            'capture_snapshots': self.convert_capture_snapshots,
        }

        ### Other options
        self.quiet = 0
        self.frontend_from_env = False
        self._allowed_email_formats = ('text', 'html')
        self._allowed_which = ('both', 'news', 'changelogs')

        self.debs = []

    @staticmethod
    def convert_capture_snapshots(value):
        if not value:
            return False
        if re.match(r'(auto|1|y(?:es)|t(?:rue)|on)$', value.lower()):
            return True
        # Snapshot capturing is supposed to be extremely unobtrusive and never
        # interfere with proper operation of the script, so anything we can't
        # definitively determine is a yes is a no.
        return False

    def setup(self, args=None, require_debs=True):
        apt_pkg.init()

        etc = apt_pkg.config.find_dir('Dir::Etc')
        conf = apt_pkg.config.find_file('Dir::Etc::apt-listchanges-main')
        # There is no practical way to unit-test these two cases until
        # pytest-subprocess is packaged so I can use it in my tests
        if not conf:  # pragma: no cover
            conf = os.path.join(etc, 'listchanges.conf')
        conf_d = apt_pkg.config.find_dir('Dir::Etc::apt-listchanges-parts')
        if conf_d == '/':  # pragma: no cover
            conf_d = os.path.join(etc, 'listchanges.conf.d')

        configs = [conf]
        configs += glob(os.path.join(conf_d, '*.conf'))
        self.read(configs)
        self.getopt(sys.argv if args is None else args,
                    require_debs=require_debs)

    def read(self, file):
        self.parser.read(file)

    def expose(self):
        if self.parser.has_section(self.profile):
            for option in self.parser.options(self.profile):
                value = None
                if option in self._bool_opts:
                    value = self.parser.getboolean(self.profile, option)
                elif option in self._value_opts or \
                        option in self._cfgfile_only_opts:
                    value = self.parser.get(self.profile, option)
                else:
                    ALCLog.warning(
                        _("Unknown configuration file option: %s") % option)
                    continue
                converter = self._cfgfile_converters.get(option, lambda v: v)
                setattr(self, option, converter(value))

    def get(self, option, defvalue=None):
        return getattr(self, option, defvalue)

    def usage(self, exitcode):
        if exitcode == 0:
            fh = sys.stdout
        else:
            fh = sys.stderr

        fh.write(
            _("Usage: apt-listchanges [options] {--apt | filename.deb ...}\n"))
        sys.exit(exitcode)

    def _check_allowed(self, arg, opt, allowed):
        if arg in allowed:
            return arg
        ALCLog.error(
            _('Unknown argument %(arg)s for option %(opt)s.  '
              'Allowed are: %(allowed)s.') %
            {'arg': arg, 'opt': opt, 'allowed': ', '.join(allowed)})
        sys.exit(1)

    def _check_debs(self, debs):
        if self.apt_mode or self.dump_seen:
            return
        if not debs:
            self.usage(1)
        for deb in debs:
            ext = os.path.splitext(deb)[1]
            if ext != ".deb":
                ALCLog.error(
                    _("%(deb)s does not have '.deb' extension") % {'deb': deb})
                sys.exit(1)
            if not os.path.isfile(deb):
                ALCLog.error(
                    _('%(deb)s does not exist or is not a file') %
                    {'deb': deb})
                sys.exit(1)
            if not os.access(deb, os.R_OK):
                ALCLog.error(_('%(deb)s is not readable') % {'deb': deb})
                sys.exit(1)

    def getopt(self, argv, require_debs=True):
        try:
            (optlist, args) = getopt.getopt(argv[1:], 'vf:s:cah', [
                # command line only
                "apt", "profile=", "help",
                # deprecated options for backward compatibility
                "all", "save_seen="]
                # boolean options
                + [x.replace('_', '-') for x in self._bool_opts]
                # with value options
                + [x.replace('_', '-')+'=' for x in self._value_opts]
            )
        except getopt.GetoptError as err:
            ALCLog.error(str(err))
            sys.exit(1)

        # Determine mode and profile before processing other options
        for opt, arg in optlist:
            if opt == '--profile':
                self.profile = arg
            elif opt == '--apt':
                self.apt_mode = True

        # Provide a default profile if none has been specified
        if self.profile is None:
            if self.apt_mode:
                self.profile = 'apt'
            else:
                self.profile = 'cmdline'

        # Expose defaults from config file
        self.expose()

        # Environment variables override config file
        if 'APT_LISTCHANGES_FRONTEND' in os.environ:
            self.frontend = os.getenv('APT_LISTCHANGES_FRONTEND')
            self.frontend_from_env = True
            # Prefer APT_LISTCHANGES_FRONTEND over DEBIAN_FRONTEND
            self.ignore_debian_frontend = True

        (since, latest, show_all) = (None, None, False)

        # Command-line options override environment and config file
        for opt, arg in optlist:
            if opt == '--help':
                self.usage(0)
            elif opt in ('-v', '--verbose'):
                self.verbose = True
            elif opt in ('-f', '--frontend'):
                self.frontend = arg
            elif opt == '--email-address':
                self.email_address = arg
            elif opt == '--email-format':
                self.email_format = self._check_allowed(
                    arg, opt, self._allowed_email_formats)
            elif opt in ('-c', '--confirm'):
                self.confirm = True
            elif opt == '--since':
                since = arg
            elif opt == '--latest':
                latest = arg
            elif opt in ('-a', '--show-all', '--all'):
                show_all = True
            elif opt in ('-h', '--headers'):
                self.headers = True
            elif opt in ('--save-seen', '--save_seen'):
                self.save_seen = arg
            elif opt == '--dump-seen':
                self.dump_seen = True
            elif opt == '--which':
                self.which = self._check_allowed(arg, opt, self._allowed_which)
            elif opt == '--debug':
                self.debug = True
            elif opt == '--reverse':
                self.reverse = True
            elif opt in ('-n', '--no-network'):
                self.no_network = True
            elif opt == '--select-frontend':
                self.select_frontend = True
            elif opt == '--ignore-apt-assume':
                self.ignore_apt_assume = True
            elif opt == '--ignore-debian-frontend':
                self.ignore_debian_frontend = True
            elif opt == '--titled':
                self.titled = True
            elif opt == '--untitled':
                self.titled = False
            elif opt == '--hide':
                self.hide = True
            elif opt == '--log':
                self.log = arg
            elif opt == '--filter':
                self.filter = arg

        if latest is not None:
            latest = int(latest)
        if self.email_address == 'none':
            self.email_address = None
        if self.save_seen == 'none':
            self.save_seen = None

        if ((self.since is not None and self.show_all)
           or (since is not None and show_all)):
            ALCLog.error(
                _('--since=<version> and --show-all are mutually '
                  'exclusive'))
            sys.exit(1)
        elif since is not None or show_all:
            self.since = since
            self.show_all = show_all

        if self.since is not None:
            if len(args) != 1:
                ALCLog.error(
                    _('--since=<version> expects a path to exactly one '
                      '.deb archive'))
                sys.exit(1)
            self.save_seen = None

        if (self.latest is not None and self.show_all) or \
           (latest is not None and show_all):
            ALCLog.error(
                _('--latest=<N> and --show-all are mutually exclusive'))
            sys.exit(1)
        elif latest is not None or show_all:
            self.latest = latest
            self.show_all = show_all

        if self.apt_mode and not self.ignore_debian_frontend and \
           os.getenv('DEBIAN_FRONTEND', '') == 'noninteractive':
            # Force non-interactive usage
            self.quiet = 1
            self.confirm = False

        self.debs = args

        if require_debs or self.debs:
            self._check_debs(self.debs)


__all__ = ['ALCConfig']
