#! /usr/bin/python
# Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
# 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 errno

import apt
import apt_pkg
import aptsources.sourceslist
from debian import deb822, debian_support

import utils


cache = apt.Cache()

srcrec = None
pkgsrc = None

re_print_uris_filename = re.compile(r"'.+?' (.+?) ")
re_comma_sep = re.compile(r'\s*,\s*')


def sources_list_path(options):
    return os.path.join(os.path.realpath(options.destdir), 'sources.list')


def lists_path(options):
    return os.path.join(os.path.realpath(options.destdir), 'lists.apt')


def pkgcache_path(options):
    return os.path.join(os.path.realpath(options.destdir), 'pkgcache.bin')


def srcpkgcache_path(options):
    return os.path.join(os.path.realpath(options.destdir), 'srcpkgcache.bin')


def apt_options(options):
    '''Return some standard APT options in command-line format.'''
    return ['-o', 'Dir::State::Lists=%s' % lists_path(options),
            '-o', 'Dir::Cache::pkgcache=%s' % pkgcache_path(options),
            '-o', 'Dir::Cache::srcpkgcache=%s' % srcpkgcache_path(options)]


def reopen_cache():
    cache.open()


def update_destdir(options):
    # We can't use this until we've stopped using apt-get to install
    # build-dependencies.
    # cache.update(sources_list='%s.destdir' % sources_list_path(options))
    command = ['apt-get']
    command.extend(apt_options(options))
    command.extend(
        ['-o', 'Dir::Etc::sourcelist=%s.destdir' % sources_list_path(options),
         '-o', 'Dir::Etc::sourceparts=#clear',
         '-o', 'APT::List-Cleanup=false',
         '-o', 'Debug::NoLocking=true',
         'update'])
    utils.spawn(command)

    reopen_cache()


apt_conf_written = False


def update_apt_repository(options, force_rebuild=False):
    global apt_conf_written
    apt_conf = os.path.join(options.destdir, 'apt.conf')
    if not apt_conf_written:
        with open(apt_conf, 'w') as apt_conf_file:
            print('''
Dir {
        ArchiveDir ".";
        CacheDir ".";
};

BinDirectory "." {
        Packages "Packages";
        BinCacheDB "pkgcache.apt";
        FileList "filelist.apt";
};''', file=apt_conf_file)
        apt_conf_written = True

    if force_rebuild:
        try:
            os.unlink(os.path.join(options.destdir, 'pkgcache.apt'))
        except OSError as e:
            if e.errno != errno.ENOENT:
                raise

    filelist = os.path.join(options.destdir, 'filelist.apt')
    with open(filelist, 'w') as filelist_file:
        for name in sorted(os.listdir(options.destdir)):
            if name.endswith('.deb'):
                print('./%s' % name, file=filelist_file)

    utils.spawn(['apt-ftparchive', 'generate', 'apt.conf'],
                cwd=options.destdir)

    update_destdir(options)


def init(options):
    """Configure APT the way we like it.  We need a custom sources.list."""
    system_sources_list = apt_pkg.config.find_file('Dir::Etc::sourcelist')
    sources_list = sources_list_path(options)
    real_destdir = os.path.realpath(options.destdir)
    with open('%s.destdir' % sources_list, 'w') as sources_list_file:
        print(('deb [trusted=yes] file:%s ./' % real_destdir),
              file=sources_list_file)
    with open(sources_list, 'w') as sources_list_file:
        print(('deb [trusted=yes] file:%s ./' % real_destdir),
              file=sources_list_file)
        try:
            with open(system_sources_list) as system_sources_list_file:
                shutil.copyfileobj(system_sources_list_file, sources_list_file)
        except IOError as e:
            if e.errno != errno.ENOENT:
                raise
    apt_pkg.config.set('Dir::Etc::sourcelist', sources_list)

    system_lists = apt_pkg.config.find_file('Dir::State::Lists')
    lists = lists_path(options)
    shutil.rmtree(lists, ignore_errors=True)
    try:
        os.makedirs(lists)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise
    for system_list in os.listdir(system_lists):
        if system_list == 'lock':
            continue
        system_list_path = os.path.join(system_lists, system_list)
        if not os.path.isfile(system_list_path):
            continue
        os.symlink(system_list_path, os.path.join(lists, system_list))
    apt_pkg.config.set('Dir::State::Lists', lists)

    apt_pkg.config.set('Dir::Cache::pkgcache', pkgcache_path(options))
    apt_pkg.config.set('Dir::Cache::srcpkgcache', srcpkgcache_path(options))

    update_apt_repository(options)


def init_src_cache():
    """Build a source package cache."""
    global srcrec, pkgsrc
    if srcrec is not None:
        return

    print("Building source package cache ...")
    srcrec = {}
    pkgsrc = {}

    version = {}
    binaries = {}

    # This is a somewhat ridiculous set of workarounds for APT's anaemic
    # source package database. The SourceRecords interface is inordinately
    # slow, because it searches the underlying database every single time
    # rather than keeping real lists; there really is, as far as I can see,
    # no proper way to ask APT for the list of downloaded Sources index
    # files in order to parse it ourselves; and so we must resort to looking
    # at the output of 'sudo apt-get --print-uris update' to get the list of
    # downloaded Sources files, and running them through python-debian.

    listdir = apt_pkg.config.find_dir('Dir::State::Lists')

    sources = []
    for line in utils.get_output_root(['apt-get', '--print-uris',
                                       'update']).splitlines():
        matchobj = re_print_uris_filename.match(line)
        if not matchobj:
            continue
        filename = matchobj.group(1)
        if filename.endswith('_Sources'):
            sources.append(filename)
            print("Using file %s for apt cache" % filename)

    for source in sources:
        try:
            with open(os.path.join(listdir, source)) as source_file:
                tag_file = apt_pkg.TagFile(source_file)
                for src_stanza in tag_file:
                    if ('package' not in src_stanza or
                            'version' not in src_stanza or
                            'binary' not in src_stanza):
                        continue
                    src = src_stanza['package']
                    if (src not in srcrec or
                        (debian_support.Version(src_stanza['version']) >
                         debian_support.Version(version[src]))):
                        srcrec[src] = str(src_stanza)
                        version[src] = src_stanza['version']
                        binaries[src] = src_stanza['binary']
        except IOError:
            continue

    for src, pkgs in binaries.items():
        for pkg in re_comma_sep.split(pkgs):
            pkgsrc[pkg] = src


class MultipleProvidesException(RuntimeError):
    pass


seen_providers = {}


def get_real_pkg(pkg):
    """Get the real name of binary package pkg, resolving Provides."""
    if pkg in cache and cache[pkg].versions:
        return pkg
    elif pkg in seen_providers:
        return seen_providers[pkg]

    providers = cache.get_providing_packages(pkg)
    if len(providers) == 0:
        seen_providers[pkg] = None
    elif len(providers) > 1:
        # If one of them is already installed, just pick one
        # arbitrarily. (Consider libstdc++-dev.)
        for provider in providers:
            if provider.is_installed:
                seen_providers[pkg] = provider.name
                break
        else:
            # Favoured virtual depends should perhaps be configurable
            # This should be debug-only
            print(("Multiple packages provide %s; arbitrarily choosing %s" %
                  (pkg, providers[0].name)))
            seen_providers[pkg] = providers[0].name
    else:
        seen_providers[pkg] = providers[0].name
    return seen_providers[pkg]


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

    real_pkg = get_real_pkg(pkg)
    if real_pkg is None:
        real_pkg = pkg
    record = get_src_record(real_pkg)
    if record is not None and 'package' in record:
        return record['package']
    else:
        return None


def get_src_record(src):
    """Return a parsed source package record for source package src."""
    init_src_cache()
    record = srcrec.get(src)
    if record is not None:
        return deb822.Sources(record)
    # try lookup by binary package
    elif src in pkgsrc and pkgsrc[src] != src:
        return deb822.Sources(srcrec.get(pkgsrc[src]))
    else:
        return None


def get_pkg_record(pkg):
    """Return a parsed binary package record for binary package pkg."""
    return deb822.Packages(str(cache[pkg].candidate.record))


def get_src_version(src):
    record = get_src_record(src)
    if record is not None:
        return record['version']
    else:
        return None


def get_src_binaries(src):
    """Return all the binaries produced by source package src."""
    record = get_src_record(src)
    if record is not None:
        bins = [b[0]['name'] for b in record.relations['binary']]
        return [b for b in bins if b in cache]
    else:
        return None


apt_architectures = None


def apt_architecture_allowed(arch):
    """Check if apt can acquire packages for the host architecture."""
    global apt_architectures

    if apt_architectures is not None:
        return arch in apt_architectures

    apt_architectures = set()
    for entry in aptsources.sourceslist.SourcesList():
        apt_architectures |= set(entry.architectures)

    return arch in apt_architectures
