#!/usr/bin/python

# Copyright (c) 2009-2011, Benjamin Drung <bdrung@debian.org>
#
# 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

_VENDOR_ENV = "DH_XUL_EXT_VENDOR"
# 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 is_same_package(self, xul_app):
        return self.xul_id == xul_app.xul_id and self.package == xul_app.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 update_version(self, sol, eol):
        if compare_versions(self.sol, sol) > 0:
            self.sol = sol
        if compare_versions(self.eol, eol) < 0:
            self.eol = eol


def _get_data_dir():
    """Get the data directory based on the module location."""
    if __file__.startswith("/usr/bin"):
        data_dir = "/usr/share/mozilla-devscripts"
    else:
        data_dir = os.path.dirname(__file__)
    return data_dir

def get_vendor():
    """This function returns the vendor (e.g. Debian, Ubuntu) that should be
       used for calculating the dependencies. DH_XUL_EXT_VENDOR will be used
       if set. Otherwise dpkg-vendor will be used for determining the vendor."""
    if _VENDOR_ENV in os.environ:
        vendor = os.environ[_VENDOR_ENV]
    else:
        cmd = ["dpkg-vendor", "--query", "Vendor"]
        process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        vendor = process.communicate()[0].strip()
    return vendor

def get_xul_apps(script_name, all_distros):
    vendor = get_vendor()
    data_dir = _get_data_dir()
    if all_distros or vendor == "all":
        csv_filenames = sorted(glob.glob(os.path.join(data_dir,
                                                      "xul-app-data.csv.*")))
    else:
        csv_filename = os.path.join(data_dir, "xul-app-data.csv." + vendor)
        if not os.path.isfile(csv_filename):
            print >> sys.stderr, ('%s: Unknown vendor "%s" specified.' %
                                  (script_name, vendor))
            sys.exit(1)
        csv_filenames = [csv_filename]

    xul_apps = []
    for csv_filename in csv_filenames:
        csvfile = open(csv_filename)
        csv_reader = csv.DictReader(csvfile)
        for row in csv_reader:
            xul_app = XulApp(row["id"], row["package"], row["sol"], row["eol"])
            existing = [x for x in xul_apps if x.is_same_package(xul_app)]
            if existing:
                xul_app = existing[0]
                xul_app.update_version(row["sol"], row["eol"])
            else:
                xul_apps.append(xul_app)

    return xul_apps

def _get_id_max_min_triple(install_rdf):
    """create array of id_max_min triples"""
    id_max_min = []
    model = RDF.Model()
    parser = RDF.Parser(name="rdfxml")
    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"]))
    return id_max_min

def get_supported_apps(script_name, xul_apps, install_rdf, package,
                       verbose=False):
    id_max_min = _get_id_max_min_triple(install_rdf)
    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 = [x for x in id_max_min if x[0] == xul_app.get_id()]
        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 = [x for x in lines if x.find("Package:") >= 0]
    packages = [p[p.find(":")+1:].strip() for p in package_lines]
    return packages

def get_source_package_name():
    source = None
    control_file = open("debian/control")
    for line in control_file:
        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 = [l for l in lines if l.find("${xpi:Depends}") >= 0]
    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 = [l for l in lines if l.find("MOZ_XPI_EXT_NAME") != -1]
    if len(lines) > 0:
        line = lines[-1]
        ext_name = line[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 = [os.path.join(path, d) for d in 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:
        if verbose:
            print script_name + ": " + package + \
                  " does not contain a XUL extension (no install.rdf found)."
        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 = [x[basepath_len:] for x in 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):
        substvars_file = open(filename)
        lines = substvars_file.readlines()
        substvars_file.close()
    else:
        lines = list()

    # remove existing varibles
    lines = [s for s in lines if not s.startswith("xpi:")]

    supported_apps = get_supported_apps(script_name, xul_apps, install_rdf,
                                        package, verbose)
    packages = [a.get_versioned_package() for a in 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 = [a.get_package() for a in 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
    substvars_file = open(filename, "w")
    substvars_file.writelines(lines)
    substvars_file.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)


def 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("-a", "--all", action="store_true", dest="all",
                      help="expand substvars to all known XUL applications "
                           "(not only of your distribution)", default=False)
    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 = parser.parse_args()[0]

    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(script_name, options.all)
    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)

if __name__ == "__main__":
    main()
