#! /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
try:
    from debian import deb822, debian_support
except ImportError:
    from debian_bundle 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 ['--allow-unauthenticated',
            '-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(
        ['--allow-unauthenticated',
         '-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:
        apt_conf_file = open(apt_conf, 'w')
        print('''
Dir {
	ArchiveDir ".";
	CacheDir ".";
};

BinDirectory "." {
	Packages "Packages";
	BinCacheDB "pkgcache.apt";
	FileList "filelist.apt";
};''', file=apt_conf_file)
        apt_conf_file.close()
        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')
    filelist_file = open(filelist, 'w')
    for name in sorted(os.listdir(options.destdir)):
        if name.endswith('.deb'):
            print('./%s' % name, file=filelist_file)
    filelist_file.close()

    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)
    sources_list_file = open('%s.destdir' % sources_list, 'w')
    print(('deb file:%s ./' % os.path.realpath(options.destdir)),
          file=sources_list_file)
    sources_list_file.close()
    sources_list_file = open(sources_list, 'w')
    print(('deb file:%s ./' % os.path.realpath(options.destdir)),
          file=sources_list_file)
    try:
        system_sources_list_file = open(system_sources_list)
        shutil.copyfileobj(system_sources_list_file, sources_list_file)
        system_sources_list_file.close()
    except IOError as e:
        if e.errno != errno.ENOENT:
            raise
    sources_list_file.close()
    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))

    apt_pkg.config.set('APT::Get::AllowUnauthenticated', str(True))

    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:
            source_file = open(os.path.join(listdir, source))
        except IOError:
            continue
        try:
            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']
        finally:
            source_file.close()

    for src, pkgs in binaries.iteritems():
        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
