#! /usr/bin/python
# Copyright (c) 2009, 2010 The Chromium OS Authors. All rights reserved.
# Copyright (c) 2010 Canonical Ltd.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Written by Colin Watson for Canonical Ltd.

from __future__ import print_function

import os
import re
import shutil
import sys
try:
    from configparser import NoOptionError, NoSectionError
    if sys.version < '3.2':
        from configparser import SafeConfigParser
    else:
        from configparser import ConfigParser as SafeConfigParser
except ImportError:
    from ConfigParser import NoOptionError, NoSectionError, SafeConfigParser

from debian import deb822, changelog


__pychecker__ = 'unusednames=cls'


re_dep = re.compile(
    r'''
        ^\s*(?P<name>[a-zA-Z0-9.+\-]{2,}|\${[^}]*})
         (\s*\(\s*(?P<relop>[>=<]+)\s*
                  (?P<version>[0-9a-zA-Z:\-+~.]+|\${[^}]*})\s*\))?
         (\s*\[(?P<archs>[\s!\w\-]+)\])?\s*$
    ''', re.X)
re_comma_sep = re.compile(r'\s*,\s*')
re_pipe_sep = re.compile(r'\s*\|\s*')
re_blank_sep = re.compile(r'\s*')


# This is derived from deb822.PkgRelations.parse_relations, but we want to
# treat substvars in an intelligent way. We make the following assumptions:
#
#   * All automatically generated dependencies will be satisfied by
#     build-dependencies too (fairly safe).
#   * Anyone that depends on an automatically generated Provides will, with
#     any luck, also build-depend on something that causes the same source
#     package to be built (less safe, but probably not too unsound).
#   * Any automatically generated versions in dependencies will normally be
#     within the same source package, and may be safely discarded.

class MyPkgRelation(deb822.PkgRelation):
    @classmethod
    def parse_relations(cls, raw):
        def parse_archs(raw):
            # assumption: no space beween '!' and architecture name
            archs = []
            for arch in re_blank_sep.split(raw.strip()):
                if len(arch) and arch[0] == '!':
                    archs.append((False, arch[1:]))
                else:
                    archs.append((True, arch))
            return archs

        def parse_rel(raw):
            match = re_dep.match(raw)
            if match:
                parts = match.groupdict()
                if parts['name'].startswith('${'):
                    return
                d = {'name': parts['name']}
                if not (parts['relop'] is None or parts['version'] is None):
                    if parts['version'].startswith('${'):
                        d['version'] = None
                    else:
                        d['version'] = (parts['relop'], parts['version'])
                else:
                    d['version'] = None
                if parts['archs'] is None:
                    d['arch'] = None
                else:
                    d['arch'] = parse_archs(parts['archs'])
                return d

        def parse_or_deps(or_deps):
            return [dep for dep in map(parse_rel, or_deps) if dep]

        tl_deps = re_comma_sep.split(raw.strip())  # top-level deps
        cnf = [re_pipe_sep.split(tl_dep) for tl_dep in tl_deps]
        return [deps for deps in map(parse_or_deps, cnf) if deps]

deb822.PkgRelation.parse_relations = MyPkgRelation.parse_relations


def get_control_lines(sequence):
    """Strip comments from a control file so that deb822 can handle them."""

    new_sequence = []
    # As well as stripping comments, make sure that there's only one blank
    # line between each stanza, and no leading blank lines.
    expect_next_stanza = True
    for line in sequence:
        if line.startswith('#'):
            continue
        if line.rstrip('\n'):
            new_sequence.append(line)
            expect_next_stanza = False
        elif not expect_next_stanza:
            new_sequence.append('\n')
            expect_next_stanza = True
    return new_sequence


srcdir = None
dirsrc = None
pkgsrc = None
srcpkgs = None
srcrec = None
pkgrec = None
provides = None


def init_cache():
    global srcdir, dirsrc, pkgsrc, srcpkgs, srcrec, pkgrec, provides

    if srcdir is None:
        srcdir = {}
        dirsrc = {}
        pkgsrc = {}
        srcpkgs = {}
        srcrec = {}
        pkgrec = {}
        provides = {}


def scan_dir(path):
    init_cache()

    if os.path.exists('%s/xdeb.cfg' % path):
        config = SafeConfigParser()
        config.read('%s/xdeb.cfg' % path)
        try:
            path = '%s/%s' % (path, config.get('Package', 'directory'))
        except (NoSectionError, NoOptionError):
            pass
        try:
            debian_symlink = config.get('Package', 'debian_symlink')
            create_symlink = True
            if os.path.islink('%s/debian' % path):
                if os.readlink('%s/debian' % path) == debian_symlink:
                    create_symlink = False
                else:
                    os.unlink('%s/debian' % path)
            elif os.path.exists('%s/debian' % path):
                shutil.rmtree('%s/debian' % path)
            if create_symlink:
                print("Creating debian -> %s symlink in %s" % (debian_symlink,
                                                               path))
                os.symlink(debian_symlink, '%s/debian' % path)
        except (NoSectionError, NoOptionError):
            pass

    try:
        with open('%s/debian/control' % path) as control_file:
            control_lines = get_control_lines(control_file)
    except IOError:
        return False

    stanzas = deb822.Deb822.iter_paragraphs(control_lines, use_apt_pkg=False)
    try:
        src_stanza = next(stanzas)
    except StopIteration:
        return False
    if 'source' not in src_stanza:
        return False
    src = src_stanza['source']
    srcdir[src] = path
    dirsrc[path] = src
    srcrec[src] = deb822.Sources(src_stanza)
    for stanza in stanzas:
        if 'package' not in stanza:
            continue
        pkg = stanza['package']
        pkgrec[pkg] = deb822.Packages(stanza)
        pkgsrc[pkg] = src
        srcpkgs.setdefault(src, [])
        srcpkgs[src].append(pkg)
        if 'provides' in stanza:
            provs = stanza['provides'].strip()
            for prov in deb822.PkgRelation.parse_relations(provs):
                if prov:
                    provides.setdefault(prov[0]['name'], [])
                    provides[prov[0]['name']].append(pkg)

    return True


def build_cache(options):
    if srcdir is not None:
        return
    init_cache()

    print("Building working tree cache ...")

    # Build cache from the current contents of the working tree.
    for builddir in options.builddirs:
        for name in sorted(os.listdir(builddir)):
            path = os.path.join(builddir, name)
            if os.path.isdir(path):
                if scan_dir(path):
                    continue
            files_path_hack = os.path.join(path, 'files')
            if os.path.isdir(files_path_hack):
                if scan_dir(files_path_hack):
                    continue
            src_path_hack = os.path.join(path, 'src')
            if os.path.isdir(src_path_hack):
                scan_dir(src_path_hack)


def get_src_directory(options, src):
    build_cache(options)
    return srcdir.get(src)


def get_directory_src(options, path):
    build_cache(options)
    return dirsrc.get(path)


class MultipleProvidesException(RuntimeError):
    pass


def get_real_pkg(options, pkg):
    """Get the real name of binary package pkg, resolving Provides."""
    build_cache(options)
    if pkg in pkgsrc:
        return pkg
    elif pkg in provides:
        if len(provides[pkg]) > 1:
            raise MultipleProvidesException(
                "Multiple packages provide %s; package must select one" % pkg)
        else:
            return provides[pkg][0]


def get_src_name(options, pkg):
    """Return the name of the source package that produces binary package
    pkg."""

    build_cache(options)
    real_pkg = get_real_pkg(options, pkg)
    if real_pkg:
        return pkgsrc[real_pkg]
    else:
        return None


def get_src_record(options, src):
    """Return a parsed source package record for source package src."""
    build_cache(options)
    if src in srcrec:
        return srcrec[src]
    else:
        return None


def get_pkg_record(options, pkg):
    """Return a parsed binary package record for binary package pkg."""
    build_cache(options)
    if pkg in pkgrec:
        return pkgrec[pkg]
    else:
        return None


def get_src_version(options, src):
    """Return the version of the working tree for source package src."""
    changelog_path = '%s/debian/changelog' % get_src_directory(options, src)
    try:
        with open(changelog_path) as changelog_file:
            cl = changelog.Changelog(file=changelog_file, max_blocks=1)
            if cl.get_versions():
                return str(cl.version)
            else:
                return None
    except IOError:
        return None


def get_src_binaries(options, src):
    """Return all the binaries produced by source package src."""
    build_cache(options)
    if src in srcpkgs:
        return srcpkgs[src]
    else:
        return None


def architectures(options, src):
    control_path = '%s/debian/control' % get_src_directory(options, src)
    try:
        with open(control_path) as control_file:
            control_lines = get_control_lines(control_file)
    except IOError:
        return set()

    architectures = set()
    # apt_pkg is quicker, but breaks if the control file contains comments.
    stanzas = deb822.Deb822.iter_paragraphs(control_lines, use_apt_pkg=False)
    next(stanzas)  # discard source stanza
    for stanza in stanzas:
        if 'architecture' not in stanza:
            architectures.add('any')
        else:
            architectures.update(stanza['architecture'].split())
    return architectures


def all_packages(options):
    build_cache(options)
    return sorted(set(srcdir) - set(options.exclude))
