#!/usr/bin/python

"""
This script generate a list of changelogs from the changes list found on
http://people.canonical.com/~ogra/touch-image-stats/
"""

# Copyright (C) 2013 Canonical
#
# Authors: Jean-Baptiste Lallement <jean-baptiste.lallement@canonical.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 3.
#
# 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.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#

import sys
from textwrap import dedent
from os import path, mkdir
from urllib2 import urlopen, HTTPError
from pprint import pprint
from launchpadlib.launchpad import Launchpad
from apt_pkg import version_compare, init as apt_init
from distro_info import UbuntuDistroInfo
import apt

OGRA = "http://people.canonical.com/~ogra/touch-image-stats/"
DEFAULT_RELEASE = UbuntuDistroInfo().devel()
APTCACHE = apt.Cache()

# Launchpad credentials
lp = None
ubuntu = None
archive = None
distros = {}
components = ('main', 'restricted', 'universe', 'multiverse')
suites = []


def usage():
    """
    Display usage an exit
    """
    print(dedent("""\
                 Usage: %s PATH
                 Read changes from PATH and generate a list of changelogs for
                 the list of packages and versions

                 Arguments
                     PATH:  Path to a changes file a found on
                            http://people.canonical.com/~ogra/touch-image-stats/current/.
                            It can be an URL or a path to a local file.
                 """) % path.basename(sys.argv[0]))


def lpinit():
    """
    Init LP credentials, archive and distro list

    Args:
        None
    """
    global lp, ubuntu, archive, distros, suites
    lp = Launchpad.login_anonymously('distro-changes', 'production')
    ubuntu = lp.distributions['ubuntu']
    archive = ubuntu.getArchive(name='primary')
    for serie in ubuntu.series:
        if serie.active:
            suites.append(serie.name)
            distros[serie.name] = serie


def get_sourcename(pkgname):
    """
    Returns a source package name

    Args:
        pkgname: Package name which we want to find source name
    Returns:
        Source package name or binary package name if source package is not
        found
    """

    try:
        pkg = APTCACHE[pkgname]
        return pkg.candidate.source_name
    except:
        print('Failed to find source for %s: %s' % (
            pkgname, sys.exc_info()[0]))
    return pkgname


def strip_epoch(verstr):
    """
    strip of the epoch from a version string

    Args:
        verstr: Package version string
    Returns:
        Version without epoch
    """
    l = verstr.split(":", 1)
    if len(l) > 1:
        return l[1]
    return verstr


def publishing_history(distroname, pkgname,
                       from_version=None, to_version=None):
    """
    Load publishing history from LP for a package and version boundaries

    Args:
        distroname: Name of the release
        pkgname: Name of a binary package
        from_version: Lower bound version
        to_version: Upper bound version
    Returns:
        List of publication inside boundaries
    """
    global archive, distros
    history = []
    last_pub = None
    from_version = strip_epoch(from_version)
    to_version = strip_epoch(to_version)
    sourcename = get_sourcename(pkgname)
    for status in ('Published', 'Superseded'):
        pubs = archive.getPublishedSources(source_name=sourcename, exact_match=True,
                                           status=status,
                                           distro_series=distros[distroname])
        for pub in pubs:
            if last_pub is None:
                last_pub = pub
            if (version_compare(from_version, pub.source_package_version) < 0 and
                    version_compare(pub.source_package_version, to_version) <= 0):
                print("%s=%s published to %s/%s on %s" % (
                    sourcename, pub.source_package_version, distroname,
                    pub.pocket, pub.date_published))
                history.append(pub)
            elif (version_compare(pub.source_package_version, from_version) <= 0):
                break  # earlier than lower bound -> Break
            else:
                pass  # out of bound -> Rejected
    # When no publication is found matching the criteria, update with the last
    # changes for this package. This is the case with the version of the binary
    # package does not correspond to the version of the source package, for
    # example with dmsetup/lvm2
    if not history:
        history.append(last_pub)
    return history


def parse_changes(uri):
    """
    Parse a changes file and returns a dict with the list of packages

    Args:
        uri: Path to a changes file. It can be an HTTP url or a local file
    Return:
        A dictionary with the format
        { 'build_from': None,   # Previous build number
          'build_to': None,     # Current build number
          'new': [],            # List of new packages
          'upgraded': [],       # List of upgraded packages
          'dropped': []         # List of dropped packages
        }
    """
    changes = {
        'build_from': None,
        'build_to': None,
        'new': [],
        'upgraded': [],
        'dropped': []
    }

    handle = None
    if uri.startswith('http://'):
        try:
            handle = urlopen(uri)
        except HTTPError as exc:
            print(exc)
    else:
        try:
            handle = open(uri,  'r')
        except FileNotFoundError as exc:
            print(exc)

    if not handle:
        return None

    change_type = None
    for line in handle:
        if type(line) == bytes:
            line = line.decode('ascii')
        line = line.strip()
        if not line:
            continue

        if line.startswith('Package changes'):
            linebits = line.split()
            changes['build_from'] = linebits[3]
            changes['build_to'] = linebits[5]
        elif line.startswith('=== '):
            linebits = line.split()
            change_type = linebits[1].lower()
        else:
            if change_type == 'upgraded':
                linebits = line.split()
                changes[change_type].append([linebits[0].split(':')[0],
                                             linebits[2], linebits[4]])
            else:
                changes[change_type].append(line)
    if handle:
        handle.close()
    return changes


def fetch_changelogs(changes):
    """
    Fetch changelogs from changelogs.ubuntu.com and update the records

    Args:
        changes: dictionary containers the list of packages that changed
    Returns:
        updated dictionary, the last element of the 'upgraded' records contains
        the changelog from the previous version to the current version
    """
    src_changes = {}
    for i in range(len(changes['upgraded'])):
        changelog = ""
        (pkg, vfrom, vto) = changes['upgraded'][i]
        src = get_sourcename(pkg)
        if src in src_changes:  # Already fetched
            continue
        pubs = publishing_history(DEFAULT_RELEASE, pkg, vfrom, vto)
        for pub in pubs:
            if pub is None:
                continue
            changesurl = pub.changesFileUrl()
            if changesurl is None:
                continue
            try:
                handle = urlopen(changesurl)
            except HTTPError as exc:
                print("E: Failed to fetch %s" % changesurl)
                print(exc)
                continue
            in_changes = False
            for line in handle:
                if not in_changes:
                    if line.startswith("Changes:"):
                        in_changes = True
                        continue
                else:
                    if not line.startswith(" "):
                        changelog += "\n"
                        break
                    else:
                        changelog += line
        src_changes[src] = [vfrom, vto, changelog]
    return src_changes


def generate_report(changes, from_build='NOID', to_build='NOID'):
    """
    Generate an HTML report of the changes

    Args:
        changes: List of changes
    Returns:
        Nothing
    """
    main_file = path.join(to_build + '.html')
    pkgs_file = path.join('content', to_build + '.packages.html')
    chgs_file = path.join('content', to_build + '.changes.html')

    if not path.exists('content'):
        mkdir('content')

    index = dedent(
        """\
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"
           "http://www.w3.org/TR/html4/frameset.dtd">
        <HTML>
        <HEAD>
        <TITLE>Changelogs from build {from_build} to {to_build}</TITLE>
        </HEAD>
        <FRAMESET cols="20%, 80%">
          <FRAME src="{pkgs}" name="packages">
          <FRAME src="{chgs}" name="changelogs">
          <NOFRAMES>
              <P>Your browser does not support frames
          </NOFRAMES>
        </FRAMESET>
        </HTML>
        """.format(from_build=from_build, to_build=to_build, pkgs=pkgs_file,
                   chgs=chgs_file))
    packages = dedent("""\
                      <head>
                        <TITLE>Changelogs from build {from_build} to {to_build}</TITLE>
                      <body><pre>
                      Full Ogra's changelist for <a href="{ogra}" target="_blank">{to_build}</a>
                      <h4>List of upgraded packages</h4><ul>""".format(
                          ogra=path.join(OGRA, "%s.changes" % to_build),
                          from_build=from_build, to_build=to_build))
    changelogs = dedent("""\
                      <head>
                        <TITLE>Changelogs from build {from_build} to {to_build}</TITLE>
                      <body>
                        <h3>Changelogs from build {from_build} to {to_build}</h3>
                        <hr>
                      """.format(from_build=from_build, to_build=to_build))

    for pkgname, change in sorted(changes.iteritems()):
        packages += dedent("""\
                           <li><a href="{chgs_file}#{pkg}" target="changelogs">{pkg}</a>
                               curr.: {vto}
                               prev.: {vfrom}
                    """.format(chgs_file=path.basename(chgs_file), pkg=pkgname, vfrom=change[0], vto=change[1]))
        chlog = change[2]
        if not chlog:
            chlog = "{pkg}\n\n  Changesfile not available.".format(pkg=pkgname)
        changelogs += dedent("""<pre><a
                             name='{pkg}'>&nbsp;</a>{changelog}</pre><hr>""".format(pkg=pkgname, changelog=chlog))
    packages += "</ul></pre></body></html>"
    changelogs += "</body></html>"

    with open(main_file, "w") as fhd:
        fhd.write(index)
    with open(pkgs_file, "w") as fhd:
        fhd.write(packages)
    with open(chgs_file, "w") as fhd:
        fhd.write(changelogs)


def generate_changelog_summary(uri):
    """
    Generate a changelog summary for the packages listed in uri

    Args:
        uri: Path to a changes file. It can be an HTTP url or a local file
    Return:
        True on success, False otherwise
    """
    changes = parse_changes(uri)
    if changes is None:
        return False

    src_changes = fetch_changelogs(changes)
    generate_report(src_changes, changes['build_from'], changes['build_to'])
    return True


def main():
    """
    Main routine
    """

    if len(sys.argv) != 2:
        usage()

    lpinit()
    apt_init()
    if not generate_changelog_summary(sys.argv[1]):
        sys.exit(1)
    sys.exit(0)

if __name__ == '__main__':
    main()
