# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import errno
import logging
import re
from os import makedirs, chmod, environ
from os.path import basename, exists, join, dirname
from sys import argv
from dhpython import DEPENDS_SUBSTVARS, PKG_NAME_TPLS, RT_LOCATIONS, RT_TPLS

log = logging.getLogger('dhpython')
parse_dep = re.compile('''[,\s]*
    (?P<name>[^ :]+)(?::any)?
    \s*
    \(?(?P<version>([>=<]{2,}|=)\s*[^\)]+)?\)?
    \s*
    (?:\[(?P<arch>[^\]]+)\])?
    ''', re.VERBOSE).match


def build_options(**options):
    """Build an Options object from kw options"""
    default_options = {
        'arch': None,
        'package': [],
        'no_package': [],
        'write_log': False,
        'remaining_packages': False,
    }
    built_options = default_options
    built_options.update(options)
    return type('Options', (object,), built_options)


class DebHelper:
    """Reinvents the wheel / some dh functionality (Perl is ugly ;-P)"""

    def __init__(self, options, impl='cpython3'):
        self.options = options
        self.packages = {}
        self.build_depends = {}
        self.python_version = None
        # Note that each DebHelper instance supports ONE interpreter type only
        # it's not possible to mix cpython2, cpython3 and pypy here
        self.impl = impl
        self.command = {
            'cpython2': 'dh_python2',
            'cpython3': 'dh_python3',
            'pypy': 'dh_pypy',
        }[impl]
        skip_tpl = set()
        for name, tpls in PKG_NAME_TPLS.items():
            if name != impl:
                skip_tpl.update(tpls)
        skip_tpl = tuple(skip_tpl)
        substvar = DEPENDS_SUBSTVARS[impl]

        pkgs = options.package
        skip_pkgs = options.no_package

        try:
            with open('debian/control', 'r', encoding='utf-8') as fp:
                paragraphs = [{}]
                field = None
                for lineno, line in enumerate(fp, 1):
                    if line.startswith('#'):
                        continue
                    if not line.strip():
                        if paragraphs[-1]:
                            paragraphs.append({})
                            field = None
                        continue
                    if line[0].isspace():  # Continuation
                        paragraphs[-1][field] += line.rstrip()
                        continue
                    if not ':' in line:
                        raise Exception(
                            'Unable to parse line %i in debian/control: %s'
                            % (lineno, line))
                    field, value = line.split(':', 1)
                    field = field.lower()
                    paragraphs[-1][field] = value.strip()
        except IOError:
            raise Exception('cannot find debian/control file')

        # Trailing new lines?
        if not paragraphs[-1]:
            paragraphs.pop()

        if len(paragraphs) < 2:
            raise Exception('Unable to parse debian/control, found less than '
                            '2 paragraphs')

        self.source_name = paragraphs[0]['source']
        if self.impl == 'cpython3' and 'x-python3-version' in paragraphs[0]:
            self.python_version = paragraphs[0]['x-python3-version']
            if len(self.python_version.split(',')) > 2:
                raise ValueError('too many arguments provided for '
                                 'X-Python3-Version: min and max only.')
        elif self.impl == 'cpython2':
            if 'x-python-version' in paragraphs[0]:
                self.python_version = paragraphs[0]['x-python-version']
            elif 'xs-python-version' in paragraphs[0]:
                self.python_version = paragraphs[0]['xs-python-version']

        build_depends = []
        for field in ('build-depends', 'build-depends-indep',
                      'build-depends-arch'):
            if field in paragraphs[0]:
                build_depends.append(paragraphs[0][field])
        build_depends = ', '.join(build_depends)
        for dep1 in build_depends.split(','):
            for dep2 in dep1.split('|'):
                details = parse_dep(dep2)
                if details:
                    details = details.groupdict()
                    if details['arch']:
                        architectures = details['arch'].split()
                    else:
                        architectures = [None]
                    for arch in architectures:
                        self.build_depends.setdefault(
                            details['name'], {})[arch] = details['version']

        for paragraph_no, paragraph in enumerate(paragraphs[1:], 2):
            if 'package' not in paragraph:
                raise Exception('Unable to parse debian/control, paragraph %i '
                                'missing Package field' % paragraph_no)
            binary_package = paragraph['package']
            if skip_tpl and binary_package.startswith(skip_tpl):
                log.debug('skipping package: %s', binary_package)
                continue
            if pkgs and binary_package not in pkgs:
                continue
            if skip_pkgs and binary_package in skip_pkgs:
                continue
            if (options.remaining_packages and
                    self.has_acted_on_package(binary_package)):
                continue
            pkg = {
                'substvars': {},
                'autoscripts': {},
                'rtupdates': [],
                'arch': paragraph['architecture'],
            }
            if (options.arch is False and pkg['arch'] != 'all' or
                    options.arch is True and pkg['arch'] == 'all'):
                # TODO: check also if arch matches current architecture:
                continue

            if not binary_package.startswith(PKG_NAME_TPLS[impl]):
                # package doesn't have common prefix (python-, python3-, pypy-)
                # so lets check if Depends/Recommends contains the
                # appropriate substvar
                if (substvar not in paragraph.get('depends', '')
                        and substvar not in paragraph.get('recommends', '')):
                    log.debug('skipping package %s (missing %s in '
                              'Depends/Recommends)',
                              binary_package, substvar)
                    continue
            # Operate on binary_package
            self.packages[binary_package] = pkg

        fp.close()
        log.debug('source=%s, binary packages=%s', self.source_name,
                  list(self.packages.keys()))

    def has_acted_on_package(self, package):
        try:
            with open('debian/{}.debhelper.log'.format(package),
                      encoding='utf-8') as f:
                for line in f:
                    if line.strip() == self.command:
                        return True
        except IOError as e:
            if e.errno != errno.ENOENT:
                raise
        return False

    def addsubstvar(self, package, name, value):
        """debhelper's addsubstvar"""
        self.packages[package]['substvars'].setdefault(name, []).append(value)

    def autoscript(self, package, when, template, args):
        """debhelper's autoscript"""
        self.packages[package]['autoscripts'].setdefault(when, {})\
            .setdefault(template, []).append(args)

    def add_rtupdate(self, package, value):
        self.packages[package]['rtupdates'].append(value)

    def save_autoscripts(self):
        for package, settings in self.packages.items():
            autoscripts = settings.get('autoscripts')
            if not autoscripts:
                continue

            for when, templates in autoscripts.items():
                fn = "debian/%s.%s.debhelper" % (package, when)
                if exists(fn):
                    with open(fn, 'r', encoding='utf-8') as datafile:
                        data = datafile.read()
                else:
                    data = ''

                new_data = ''
                for tpl_name, args in templates.items():
                    for i in args:
                        # try local one first (useful while testing dh_python3)
                        fpath = join(dirname(__file__), '..',
                                     "autoscripts/%s" % tpl_name)
                        if not exists(fpath):
                            fpath = "/usr/share/debhelper/autoscripts/%s" % tpl_name
                        with open(fpath, 'r', encoding='utf-8') as tplfile:
                            tpl = tplfile.read()
                        if self.options.compile_all and args:
                            # TODO: should args be checked to contain dir name?
                            tpl = tpl.replace('-p #PACKAGE#', '')
                        elif settings['arch'] == 'all':
                            tpl = tpl.replace('#PACKAGE#', package)
                        else:
                            arch = environ['DEB_HOST_ARCH']
                            tpl = tpl.replace('#PACKAGE#', '%s:%s' % (package, arch))
                        tpl = tpl.replace('#ARGS#', i)
                        if tpl not in data and tpl not in new_data:
                            new_data += "\n%s" % tpl
                if new_data:
                    data += '\n# Automatically added by {}'.format(basename(argv[0])) +\
                            '{}\n# End automatically added section\n'.format(new_data)
                    fp = open(fn, 'w', encoding='utf-8')
                    fp.write(data)
                    fp.close()

    def save_substvars(self):
        for package, settings in self.packages.items():
            substvars = settings.get('substvars')
            if not substvars:
                continue
            fn = "debian/%s.substvars" % package
            if exists(fn):
                with open(fn, 'r', encoding='utf-8') as datafile:
                    data = datafile.read()
            else:
                data = ''
            for name, values in substvars.items():
                p = data.find("%s=" % name)
                if p > -1:  # parse the line and remove it from data
                    e = data[p:].find('\n')
                    line = data[p + len("%s=" % name):
                                p + e if e > -1 else None]
                    items = [i.strip() for i in line.split(',') if i]
                    if e > -1 and data[p + e:].strip():
                        data = "%s\n%s" % (data[:p], data[p + e:])
                    else:
                        data = data[:p]
                else:
                    items = []
                for j in values:
                    if j not in items:
                        items.append(j)
                if items:
                    if data:
                        data += '\n'
                    data += "%s=%s\n" % (name, ', '.join(items))
            data = data.replace('\n\n', '\n')
            if data:
                fp = open(fn, 'w', encoding='utf-8')
                fp.write(data)
                fp.close()

    def save_rtupdate(self):
        for package, settings in self.packages.items():
            pkg_arg = '' if self.options.compile_all else "-p %s" % package
            values = settings.get('rtupdates')
            if not values:
                continue
            d = 'debian/{}/{}'.format(package, RT_LOCATIONS[self.impl])
            if not exists(d):
                makedirs(d)
            fn = "%s/%s.rtupdate" % (d, package)
            if exists(fn):
                data = open(fn, 'r', encoding='utf-8').read()
            else:
                data = "#! /bin/sh\nset -e"
            for dname, args in values:
                cmd = RT_TPLS[self.impl].format(pkg_arg=pkg_arg,
                                                dname=dname,
                                                args=args)
                if cmd not in data:
                    data += "\n%s" % cmd
            if data:
                fp = open(fn, 'w', encoding='utf-8')
                fp.write(data)
                fp.close()
                chmod(fn, 0o755)

    def save_log(self):
        if not self.options.write_log:
            return
        for package, settings in self.packages.items():
            with open('debian/{}.debhelper.log'.format(package),
                      'a', encoding='utf-8') as f:
                f.write(self.command + '\n')

    def save(self):
        self.save_substvars()
        self.save_autoscripts()
        self.save_rtupdate()
        self.save_log()
