#!/usr/bin/python
# -*- coding: utf-8 -*-
# ##################################################################
#
# Copyright (C) 2010-2011, Evan Broder <evan@ebroder.net>
# Copyright (C) 2010, Benjamin Drung <bdrung@ubuntu.com>
#
# 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; version 2.
#
# 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.
#
# See file /usr/share/common-licenses/GPL-2 for more details.
#
# ##################################################################

import optparse
import os
import shutil
import sys
import tempfile

import lsb_release

from devscripts.logger import Logger

from ubuntutools.archive import (SourcePackage, DebianSourcePackage,
                                 UbuntuSourcePackage, DownloadError)
from ubuntutools.config import UDTConfig, ubu_email
from ubuntutools.builder import get_builder
from ubuntutools.lp.lpapicache import (Launchpad, Distribution,
                                       SeriesNotFoundException,
                                       PackageNotFoundException)
from ubuntutools.misc import (system_distribution, vendor_to_distroinfo,
                              codename_to_distribution)
from ubuntutools.question import YesNoQuestion
from ubuntutools import subprocess

def error(msg):
    Logger.error(msg)
    sys.exit(1)

def check_call(cmd, *args, **kwargs):
    Logger.command(cmd)
    ret = subprocess.call(cmd, *args, **kwargs)
    if ret != 0:
        error('%s returned %d.' % (cmd[0], ret))

def parse(args):
    usage = 'Usage: %prog [options] <source package name or .dsc URL/file>'
    parser = optparse.OptionParser(usage)
    parser.add_option('-d', '--destination',
                      metavar='DEST',
                      dest='dest_releases',
                      default=[],
                      action='append',
                      help='Backport to DEST release '
                           '(default: current release)')
    parser.add_option('-s', '--source',
                      metavar='SOURCE',
                      dest='source_release',
                      help='Backport from SOURCE release '
                           '(default: devel release)')
    parser.add_option('-S', '--suffix',
                      metavar='SUFFIX',
                      help='Suffix to append to version number '
                           '(default: ~ppa1 when uploading to a PPA)')
    parser.add_option('-b', '--build',
                      default=False,
                      action='store_true',
                      help='Build the package before uploading '
                           '(default: %default)')
    parser.add_option('-B', '--builder',
                      metavar='BUILDER',
                      help='Specify the package builder (default: pbuilder)')
    parser.add_option('-U', '--update',
                      default=False,
                      action='store_true',
                      help='Update the build environment before '
                           'attempting to build')
    parser.add_option('-u', '--upload',
                      metavar='UPLOAD',
                      help='Specify an upload destination')
    parser.add_option('-y', '--yes',
                      dest='prompt',
                      default=True,
                      action='store_false',
                      help='Do not prompt before uploading to a PPA')
    parser.add_option('-v', '--version',
                      metavar='VERSION',
                      help='Package version to backport (or verify)')
    parser.add_option('-w', '--workdir',
                      metavar='WORKDIR',
                      help='Specify a working directory '
                           '(default: temporary dir)')
    parser.add_option('-r', '--release-pocket',
                      default=False,
                      action='store_true',
                      help='Target the release pocket in the .changes file. '
                           'Necessary (and default) for uploads to PPAs')
    parser.add_option('-m', '--mirror',
                      metavar='INSTANCE',
                      help='Preferred mirror (default: Launchpad)')
    parser.add_option('-l', '--lpinstance',
                      metavar='INSTANCE',
                      help='Launchpad instance to connect to '
                           '(default: production)')
    parser.add_option('--no-conf',
                      default=False,
                      action='store_true',
                      help="Don't read config files or environment variables")

    opts, args = parser.parse_args(args)
    if len(args) != 1:
        parser.error('You must specify a single source package or a .dsc '
                     'URL/path.')
    config = UDTConfig(opts.no_conf)
    if opts.builder is None:
        opts.builder = config.get_value('BUILDER')
    if not opts.update:
        opts.update = config.get_value('UPDATE_BUILDER', boolean=True)
    if opts.workdir is None:
        opts.workdir = config.get_value('WORKDIR')
    if opts.lpinstance is None:
        opts.lpinstance = config.get_value('LPINSTANCE')
    if not opts.upload and not opts.workdir:
        parser.error('Please specify either a working dir or an upload target!')
    if opts.upload and opts.upload.startswith('ppa:'):
        opts.release_pocket = True

    return opts, args, config


def find_release_package(mirror, workdir, package, version, source_release,
                         config):
    srcpkg = None

    if source_release:
        distribution = codename_to_distribution(source_release)
        if not distribution:
            error('Unknown release codename %s' % source_release)
        info = vendor_to_distroinfo(distribution)()
        source_release = info.codename(source_release, default=source_release)
    else:
        distribution = system_distribution()
    mirrors = [mirror] if mirror else []

    mirrors.append(config.get_value('%s_MIRROR' % distribution.upper()))

    if not version:
        archive = Distribution(distribution.lower()).getArchive()
        try:
            spph = archive.getSourcePackage(package, source_release)
        except (SeriesNotFoundException, PackageNotFoundException), e:
            error(str(e))
        version = spph.getVersion()

    if distribution == 'Debian':
        srcpkg = DebianSourcePackage(package,
                                     version,
                                     workdir=workdir,
                                     mirrors=mirrors)
    elif distribution == 'Ubuntu':
        srcpkg = UbuntuSourcePackage(package,
                                     version,
                                     workdir=workdir,
                                     mirrors=mirrors)

    return srcpkg

def find_package(mirror, workdir, package, version, source_release, config):
    "Returns the SourcePackage"
    if package.endswith('.dsc'):
        return SourcePackage(version=version, dscfile=package,
                             workdir=workdir, mirrors=(mirror,))

    if not source_release and not version:
        info = vendor_to_distroinfo(system_distribution())
        source_release = info().devel()

    srcpkg = find_release_package(mirror, workdir, package, version,
                                  source_release, config)
    if version and srcpkg.version != version:
        error('Requested backport of version %s but version of %s in %s is %s'
              % (version, package, source_release, srcpkg.version))

    return srcpkg

def get_backport_version(version, suffix, upload, release):
    backport_version = version + ('~%s1' % release)
    if suffix is not None:
        backport_version += suffix
    elif upload and upload.startswith('ppa:'):
        backport_version += '~ppa1'
    return backport_version

def get_backport_dist(release, release_pocket):
    if release_pocket:
        return release
    else:
        return '%s-backports' % release

def do_build(workdir, dsc, release, builder, update):
    builder = get_builder(builder)
    if not builder:
        return

    if update:
        if 0 != builder.update(release):
            sys.exit(1)

    # builder.build is going to chdir to buildresult:
    workdir = os.path.realpath(workdir)
    return builder.build(os.path.join(workdir, dsc),
                         release,
                         os.path.join(workdir, "buildresult"))

def do_upload(workdir, package, bp_version, changes, upload, prompt):
    print 'Please check %s %s in file://%s carefully!' % \
          (package, bp_version, workdir)
    if prompt or upload == 'ubuntu':
        question = 'Do you want to upload the package to %s' % upload
        answer = YesNoQuestion().ask(question, "yes")
        if answer == "no":
            return

    check_call(['dput', upload, changes], cwd=workdir)

def do_backport(workdir, pkg, suffix, release, release_pocket, build, builder,
                update, upload, prompt):
    dirname = '%s-%s' % (pkg.source, release)
    pkg.unpack(dirname)
    srcdir = os.path.join(workdir, dirname)

    bp_version = get_backport_version(pkg.version.full_version, suffix,
                                      upload, release)
    bp_dist = get_backport_dist(release, release_pocket)

    check_call(['dch',
                '--force-bad-version',
                '--force-distribution',
                '--preserve',
                '--newversion', bp_version,
                '--distribution', bp_dist,
                'No-change backport to %s' % release],
               cwd=srcdir)
    check_call(['debuild', '--no-lintian', '-S', '-nc', '-sa'], cwd=srcdir)

    fn_base = pkg.source + '_' + bp_version.split(':', 1)[-1]

    if build:
        if 0 != do_build(workdir, fn_base + '.dsc', release, builder, update):
            sys.exit(1)
    if upload:
        do_upload(workdir, pkg.source, bp_version, fn_base + '_source.changes',
                  upload, prompt)

    shutil.rmtree(srcdir)

def main(args):
    ubu_email()

    opts, (package_or_dsc,), config = parse(args[1:])

    Launchpad.login_anonymously(service=opts.lpinstance)

    if not opts.dest_releases:
        distinfo = lsb_release.get_distro_information()
        try:
            opts.dest_releases = [distinfo['CODENAME']]
        except KeyError:
            error('No destination release specified and unable to guess yours.')

    if opts.workdir:
        workdir = os.path.expanduser(opts.workdir)
    else:
        workdir = tempfile.mkdtemp(prefix='backportpackage-')

    if not os.path.exists(workdir):
        os.makedirs(workdir)

    try:
        pkg = find_package(opts.mirror,
                           workdir,
                           package_or_dsc,
                           opts.version,
                           opts.source_release,
                           config)
        pkg.pull()

        for release in opts.dest_releases:
            do_backport(workdir,
                        pkg,
                        opts.suffix,
                        release,
                        opts.release_pocket,
                        opts.build,
                        opts.builder,
                        opts.update,
                        opts.upload,
                        opts.prompt)
    except DownloadError, e:
        error(str(e))
    finally:
        if not opts.workdir:
            shutil.rmtree(workdir)

if __name__ == '__main__':
    sys.exit(main(sys.argv))