#!/usr/bin/python

# Copyright (c) 2009-2010, Benjamin Drung <bdrung@ubuntu.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import csv
import glob
import optparse
import os
import subprocess
import sys

from moz_version import compare_versions, convert_moz_to_debian_version

import RDF

# error codes
COMMAND_LINE_SYNTAX_ERROR = 1
MULTIPLE_INSTALL_RDFs = 2

class XulApp(object):
    def __init__(self, xul_id, package, sol, eol):
        self.xul_id = xul_id
        self.package = package
        self.sol = sol
        self.eol = eol
        self.min_version = None
        self.max_version = None

    def __str__(self):
        return self.xul_id + ": " + self.package + " (" + self.sol + " to " + \
               self.eol + ")"

    def get_eol(self):
        return self.eol

    def get_id(self):
        return self.xul_id

    def get_package(self):
        return self.package

    def get_sol(self):
        return self.sol

    def get_versioned_package(self):
        versioned_package = self.package
        if self.min_version:
            deb_min_version = convert_moz_to_debian_version(self.min_version)
            versioned_package += " (>= " + deb_min_version + ")"
        return versioned_package

    def set_max_version(self, max_version):
        if compare_versions(self.eol, max_version) > 0:
            self.max_version = max_version

    def set_min_version(self, min_version):
        if compare_versions(self.sol, min_version) < 0:
            self.min_version = min_version


def get_xul_apps():
    csvfile = open("/usr/share/mozilla-devscripts/xul-app-data.csv")
    csv_reader = csv.DictReader(csvfile)
    xul_apps = []
    for row in csv_reader:
        xul_app = XulApp(row["id"], row["package"], row["sol"], row["eol"])
        xul_apps.append(xul_app)
    return xul_apps

def get_supported_apps(script_name, xul_apps, install_rdf, package,
                       verbose=False):
    # create array of id_max_min triples
    id_max_min = []
    model = RDF.Model()
    parser = RDF.Parser(name="rdfxml")
    stream = parser.parse_into_model(model, "file:" + install_rdf)
    query = RDF.Query(
      """
        PREFIX em: <http://www.mozilla.org/2004/em-rdf#>
        SELECT ?id ?max ?min
        WHERE {
            ?x1 em:targetApplication ?x2 .
            ?x2 em:id ?id .
            OPTIONAL {
                ?x2 em:maxVersion ?max .
                ?x2 em:minVersion ?min .
            } .
        }
      """, query_language="sparql")
    results = query.execute(model)
    # append to id_max_min tripe to array
    for target in results:
        id_max_min.append ((target["id"].literal_value["string"],
                            target["max"].literal_value["string"],
                            target["min"].literal_value["string"]))

    if verbose:
        print "%s: %s supports %i XUL application(s):" % (script_name, package,
                                                          len(id_max_min))
        for (appid, max_version, min_version) in id_max_min:
            print "%s %s to %s" % (appid, min_version, max_version)

    # find supported apps/packages
    supported_apps = list()
    for xul_app in xul_apps:
        supported_app = filter(lambda x: x[0] == xul_app.get_id(), id_max_min)
        if len(supported_app) == 1:
            # package is supported by extension
            (appid, max_version, min_version) = supported_app.pop()
            if compare_versions(xul_app.get_sol(), max_version) <= 0:
                if compare_versions(xul_app.get_eol(), min_version) >= 0:
                    xul_app.set_min_version(min_version)
                    xul_app.set_max_version(max_version)
                    supported_apps.append(xul_app)
                    if verbose:
                        print "%s: %s supports %s." % (script_name, package,
                                                       xul_app.get_package())
                elif verbose:
                    print "%s: %s does not support %s (any more)." % \
                          (script_name, package, xul_app.get_package())
            elif verbose:
                print "%s: %s does not support %s (yet)." % \
                      (script_name, package, xul_app.get_package())
        elif len(supported_app) > 1:
            print ("%s: Found error in %s. There are multiple entries for "
                   "application ID %s.") % (script_name, install_rdf,
                                            xul_app.get_id())

    return supported_apps

def get_all_packages():
    lines = open("debian/control").readlines()
    package_lines = filter(lambda x: x.find("Package:") >= 0, lines)
    packages = map(lambda x: x[x.find(":")+1:].strip(), package_lines)
    return packages

def get_source_package_name():
    source = None
    f = open("debian/control")
    for line in f:
        if line.startswith("Source:"):
            source = line[line.find(":")+1:].strip()
            break
    return source

def has_no_xpi_depends():
    lines = open("debian/control").readlines()
    xpi_depends_lines = filter(lambda x: x.find("${xpi:Depends}") >= 0, lines)
    return len(xpi_depends_lines) == 0

def get_provided_package_names(package, supported_apps):
    ext_name = package
    for prefix in ("firefox-", "iceweasel-", "mozilla-", "xul-ext-"):
        if ext_name.startswith(prefix):
            ext_name = ext_name[len(prefix):]

    # check if MOZ_XPI_EXT_NAME is defined in debian/rules
    lines = open("debian/rules").readlines()
    lines = filter(lambda x: x.find("MOZ_XPI_EXT_NAME") != -1, lines)
    if len(lines) > 0:
        ext_name = lines[-1][line.find("=")+1:].strip()

    provides = set()
    provides.add("xul-ext-" + ext_name)
    if ext_name == get_source_package_name():
        provides.add(ext_name)

    for xul_app in supported_apps:
        app = xul_app.get_package()
        for i in xrange(len(app) - 1, -1, -1):
            if app[i] == '-':
                app = app[:i]
            elif not app[i].isdigit() and not app[i] == '.':
                break
        provides.add(app + "-" + ext_name)

    # remove package name from provide list
    provides.discard(package)

    return list(provides)

def find_install_rdfs(path):
    install_rdfs = set()

    if os.path.isfile(path) and os.path.basename(path) == "install.rdf":
        install_rdfs.add(os.path.realpath(path))

    if os.path.isdir(path):
        # recursive walk
        content = map(lambda d: os.path.join(path, d), os.listdir(path))
        install_rdfs = reduce(lambda x, d: x.union(find_install_rdfs(d)),
                              content, install_rdfs)

    return install_rdfs

def generate_substvars(script_name, xul_apps, package, verbose=False):
    install_rdfs = find_install_rdfs("debian/" + package)
    if len(install_rdfs) == 0:
        # this package does not contain a xul extension
        return
    elif len(install_rdfs) > 1:
        print >> sys.stderr, ("%s: %s contains multiple install.rdf files. "
                              "That's not supported.") % (script_name, package)
        basepath_len = len(os.path.realpath("debian/" + package))
        rdfs = map(lambda x: x[basepath_len:], install_rdfs)
        print >> sys.stderr, "\n".join(rdfs)
        sys.exit(MULTIPLE_INSTALL_RDFs)
    install_rdf = install_rdfs.pop()

    filename = "debian/" + package + ".substvars"
    if os.path.exists(filename):
        f = open(filename)
        lines = f.readlines()
        f.close()
    else:
        lines = list()

    # remove existing varibles
    lines = filter(lambda s: not s.startswith("xpi:"), lines)

    supported_apps = get_supported_apps(script_name, xul_apps, install_rdf,
                                        package, verbose)
    packages = map(lambda a: a.get_versioned_package(), supported_apps)
    if has_no_xpi_depends():
        # Use xpi:Recommends instead of xpi:Depends for backwards compatibility
        print ("%s: Warning: Please add ${xpi:Depends} to Depends. Using only "
               "${xpi:Recommends} is deprecated.") % (script_name)
        lines.append("xpi:Recommends=" + " | ".join(packages) + "\n")
    else:
        lines.append("xpi:Depends=" + " | ".join(packages) + "\n")
        lines.append("xpi:Recommends=\n")
    packages = map(lambda a: a.get_package(), supported_apps)
    lines.append("xpi:Enhances=" + ", ".join(sorted(packages)) + "\n")
    packages = get_provided_package_names(package, supported_apps)
    lines.append("xpi:Provides=" + ", ".join(sorted(packages)) + "\n")

    # write new variables
    f = open(filename, "w")
    f.writelines(lines)
    f.close()


class UnknownOptionIgnoringOptionParser(optparse.OptionParser):
    def __init__ (self, **options):
        optparse.OptionParser.__init__(self, **options)
        self.unknown_options = []

    def _process_long_opt(self, rargs, values):
        option = rargs[0].split("=")[0]
        if not option in self._long_opt:
            self.unknown_options.append(option)
            del rargs[0]
        else:
            optparse.OptionParser._process_long_opt(self, rargs, values)

    def _process_short_opts(self, rargs, values):
        option = rargs[0][0:2]
        if not self._short_opt.get(option):
            self.unknown_options.append(option)
            del rargs[0]
        else:
            optparse.OptionParser._process_short_opts(self, rargs, values)


if __name__ == "__main__":
    script_name = os.path.basename(sys.argv[0])
    epilog = "See %s(1) for more info." % (script_name)
    parser = UnknownOptionIgnoringOptionParser(epilog=epilog)
    parser.add_option("-p", "--package", dest="packages", metavar="PACKAGE",
                      action="append", default=[],
                      help="calculate substvars only for the specified PACKAGE")
    parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
                      default=False, help="print more information")

    (options, args) = parser.parse_args()

    if len(options.packages) == 0:
        options.packages = get_all_packages()

    if options.verbose:
        for unknown_option in parser.unknown_options:
            sys.stderr.write("%s: warning: no such option: %s\n" % \
                             (script_name, unknown_option))
        print script_name + ": packages:", ", ".join(options.packages)

    xul_apps = get_xul_apps()
    if options.verbose and len(xul_apps) > 0:
        print script_name + ": found %i Xul applications:" % (len(xul_apps))
        for xul_app in xul_apps:
            print xul_app

    for package in options.packages:
        generate_substvars(script_name, xul_apps, package, options.verbose)
