#!/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

from launchpadlib.launchpad import Launchpad
import lsb_release

from debian.debian_support import Version

from devscripts.logger import Logger

from ubuntutools.archive import (SourcePackage, DebianSourcePackage,
                                 UbuntuSourcePackage, DownloadError, rmadison)
from ubuntutools.config import UDTConfig, ubu_email
from ubuntutools.builder import get_builder
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',
                      dest='dest_releases',
                      default=[],
                      action='append',
                      help='Backport to DEST release '
                           '(default: current release)',
                      metavar='DEST')
    parser.add_option('-s', '--source',
                      dest='source_release',
                      default=None,
                      help='Backport from SOURCE release '
                           '(default: devel release)',
                      metavar='SOURCE')
    parser.add_option('-S', '--suffix',
                      dest='suffix',
                      default=None,
                      help='Suffix to append to version number '
                           '(default: ~ppa1)',
                      metavar='SUFFIX')
    parser.add_option('-b', '--build',
                      dest='build',
                      default=False,
                      action='store_true',
                      help='Build the package before uploading '
                           '(default: %default)')
    parser.add_option('-B', '--builder',
                      dest='builder',
                      default=None,
                      help='Specify the package builder (default: pbuilder)',
                      metavar='BUILDER')
    parser.add_option('-U', '--update',
                      dest='update',
                      default=False,
                      action='store_true',
                      help='Update the build environment before '
                           'attempting to build')
    parser.add_option('-u', '--upload',
                      dest='upload',
                      help='Specify an upload destination',
                      metavar='UPLOAD')
    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',
                      dest='version',
                      default=None,
                      help='Package version to backport (or verify)',
                      metavar='VERSION')
    parser.add_option('-w', '--workdir',
                      dest='workdir',
                      default=None,
                      help='Specify a working directory '
                           '(default: temporary dir)',
                      metavar='WORKDIR')
    parser.add_option('-m', '--mirror',
                      dest='mirror',
                      default=None,
                      help='Preferred mirror (default: Launchpad)',
                      metavar='INSTANCE')
    parser.add_option('-l', '--lpinstance',
                      dest='lpinstance',
                      default=None,
                      help='Launchpad instance to connect to '
                           '(default: production)',
                      metavar='INSTANCE')
    parser.add_option('--no-conf',
                      dest='no_conf',
                      default=False,
                      help="Don't read config files or environment variables",
                      action='store_true')

    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!')

    return opts, args, config

def get_current_version(package, distribution, source_release):
    info = vendor_to_distroinfo(distribution)
    source_release = info().codename(source_release, default=source_release)

    latest_version = None

    releases = [source_release]
    if distribution.lower() == "ubuntu":
        releases += [source_release + "-updates", source_release + "-security"]

    for release in releases:
        for record in rmadison(distribution.lower(), package, suite=release):
            if 'source' not in record:
                continue

            if (not latest_version or
                Version(latest_version) < Version(record['version'])):
                latest_version = record['version']

    return latest_version

def find_release_package(launchpad, 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)
    else:
        distribution = system_distribution()
    mirrors = [mirror] if mirror else []

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

    if not version:
        version = get_current_version(package, distribution, source_release)

    if not version:
        error('Unable to find package %s in release %s.' %
              (package, source_release))

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

    return srcpkg

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

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

    srcpkg = find_release_package(launchpad, 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(upload, release):
    if not upload or upload == 'ubuntu':
        return '%s-backports' % release
    else:
        return 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, 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(upload, release)

    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', '-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:])

    script_name = os.path.basename(sys.argv[0])
    launchpad = Launchpad.login_anonymously(script_name, 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(launchpad,
                           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.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))