#! /usr/bin/env python
# (c) Copyright 2010. CodeWeavers, Inc.

import os

# Portable which(1) implementation
def which(path, app):
    """Looks for an executable in the specified directory list.

    path is an os.pathsep-separated list of directories and app is the
    executable name. If app contains a path separator then path is ignored.
    If the file is not found, then None is returned.
    """
    if os.path.isabs(app):
        if os.path.isfile(app) and os.access(app, os.X_OK):
            return app
    elif os.sep in app or (os.altsep and os.altsep in app):
        app_path = os.path.join(os.getcwd(), app)
        if os.path.isfile(app_path) and os.access(app_path, os.X_OK):
            return app_path
    else:
        for directory in path.split(os.pathsep):
            if directory == "":
                continue
            app_path = os.path.join(directory, app)
            if os.path.isfile(app_path) and os.access(app_path, os.X_OK):
                return app_path
    return None

import sys
def locate_cx_root():
    """Locate where CrossOver is installed.

    We start by locating our own python script file and walking back up the
    path, traversing symbolic links on the way. Then we verify what we have
    found the right directory by checking for the presence of the cxmenu
    script.
    """
    # pylint: disable-msg=I0011,W0601,W0603
    global CX_ROOT
    if "CX_DEVELOP_ROOT" in os.environ:
        CX_ROOT = os.environ["CX_DEVELOP_ROOT"]
        return

    # figure out argv0
    argv0 = which(os.environ["PATH"], sys.argv[0])
    if not argv0:
        argv0 = sys.argv[0]
        if not os.path.isabs(argv0):
            argv0 = os.path.join(os.getcwd(), argv0)

    # traverse the symbolic links
    dir0 = os.path.dirname(argv0)
    while True:
        if dir0.endswith("/lib"):
            bindir = dir0[0:-3] + "bin"
        else:
            bindir = dir0
        landmark = os.path.join(bindir, "cxmenu")
        if os.path.isfile(landmark):
            break
        if not os.path.islink(argv0):
            break
        argv0 = os.readlink(argv0)
        if not os.path.isabs(argv0):
            argv0 = os.path.join(dir0, argv0)
        dir0 = os.path.dirname(argv0)

    # compute CX_ROOT
    CX_ROOT = os.path.dirname(os.path.normpath(bindir))

    # check CX_ROOT
    landmark = os.path.join(CX_ROOT, "bin", "cxmenu")
    if not os.path.isfile(landmark) or not os.access(landmark, os.X_OK):
        sys.stderr.write("%s:error: could not find CrossOver in '%s'\n" % (os.path.dirname(sys.argv[0]), CX_ROOT))
        sys.exit(1)

    sys.path.append(os.path.join(CX_ROOT, "lib", "python"))

locate_cx_root()
import cxutils
cxutils.CX_ROOT = CX_ROOT


import c4parser
import c4profiles
import crossover
import cxlog
import cxopt


TIE_VERSION = 1


#
# Association installation / uninstallation
#

def install():
    if os.path.exists(os.path.join(cxutils.CX_ROOT, 'share', 'crossover', 'data', 'cxtie.assoc')):
        cxassoc = os.path.join(cxutils.CX_ROOT, 'bin', 'cxassoc')
        if cxutils.system((cxassoc, '--tie', '--install')):
            return 1
    return 0


def uninstall():
    if os.path.exists(os.path.join(cxutils.CX_ROOT, 'share', 'crossover', 'data', 'cxtie.assoc')):
        cxassoc = os.path.join(cxutils.CX_ROOT, 'bin', 'cxassoc')
        if cxutils.system((cxassoc, '--tie', '--uninstall')):
            return 1
    return 0


#
# Product registration / unregistration
#

def _get_product_key(config, cxroot):
    """Figures out which entry contains the current product's information."""
    for key, section in config.iteritems():
        if section.get('Root', None) == cxroot:
            return key
    return None


def _get_best_tie(config, product_key=None):
    """Picks (one of) the product(s) with the highest TieVersion."""
    best_version = 0
    best_key = None
    best_is_current = False
    for key, section in config.iteritems():
        try:
            version = int(section.get('TieVersion', '0'), 10)
            # Pick the currently installed tie handler in case of a version tie!
            # And pick product_key as the second best
            installed = section.get('TieInstalled', 'false') == 'true'
            if version > best_version or \
                    (version == best_version and installed) or \
                    (version == best_version and key.lower() == product_key and not best_is_current):
                best_version = version
                best_key = key
                best_is_current = installed
        except TypeError:
            pass
    return best_key


def _get_current_tie(config):
    """Returns the key of the currently installed tie handler."""
    for key, section in config.iteritems():
        if section.get('TieInstalled', 'false') == 'true':
            return key
    return None


def _run_action(section, action):
    environ = os.environ.copy()
    environ['CX_ROOT'] = section['Root']

    # Split the command and only then expand the individual args to avoid
    # trouble with quoting.
    argv = []
    for arg in cxutils.cmdlinetoargv(section[action]):
        argv.append(cxutils.expand_unix_string(environ, arg))
    if cxutils.system(argv):
        return 1
    return 0


def register(cxroot):
    """Adds a product to the list of registered products.

    Also installs its .tie file handler, uninstalling that of another product
    if appropriate."""
    if cxroot is None:
        cxroot = cxutils.CX_ROOT
    else:
        cxroot = os.path.abspath(cxroot)

    if cxroot == cxutils.CX_ROOT:
        product = crossover.this_product()
        product_tie_version = TIE_VERSION
    else:
        product = crossover.product_info_from_cxroot(cxroot)
        if product is None:
            cxlog.err("'%s' does not contain a CrossOver product with a version >= 9.x" % cxroot)
            return 1
        try:
            if int(product['productversion'].split('.', 1)[0]) >= 10:
                # This is not a legacy version, so call its cxtie.
                cxtie = os.path.join(cxroot, 'bin', 'cxtie')
                if cxutils.system((cxtie, '--register')) == 0:
                    return 0
        except TypeError:
            pass
        product_tie_version = 0

    config = crossover.get_cross_product_config()
    wconfig = config.get_save_config()
    wconfig.lock_file()

    # Pick a unique key for the current product
    product_key = _get_product_key(config, cxroot)
    if product_key is None:
        i = 1
        while True:
            product_key = 'Product-%d-%d' % (os.geteuid(), i)
            if product_key not in config:
                break
            i += 1
    product_key = product_key.lower()

    # Register the product in the shared configuration file
    section = wconfig[product_key]
    section['Root'] = product['root']
    section['Name'] =  product['name']
    section['PublicVersion'] = product['publicversion']
    section['ProductVersion'] = product['productversion']
    section['ProductID'] = product['productid']
    section['TieVersion'] = "%d" % product_tie_version
    if cxroot == cxutils.CX_ROOT:
        section['TieInstall'] =  '"${CX_ROOT}/bin/%s" --install' % cxlog.name0()
        section['TieUninstall'] = '"${CX_ROOT}/bin/%s" --uninstall' % cxlog.name0()
    else:
        section['TieInstall'] = 'false'
        section['TieUninstall'] = 'true'

    # Then check if we should install our trampoline
    if cxroot == cxutils.CX_ROOT and \
            section.get('TieInstalled', 'false') != 'true':
        best_tie = _get_best_tie(config, product_key)
        # Don't try to set ourselves as the default handler if we cannot
        # uninstall the current one (typically because it belongs to root)
        current_tie = _get_current_tie(config)
        if best_tie == product_key and \
                (current_tie is None or current_tie in wconfig):
            if current_tie is not None and 'TieUninstall' in wconfig[current_tie]:
                _run_action(config[current_tie], 'TieUninstall')
                del wconfig[current_tie]['TieInstalled']
            install()
            section['TieInstalled'] = 'true'

    # Unlock the file last to avoid races in the install/uninstall steps
    wconfig.save_and_unlock_file()


def unregister(cxroot):
    """Removes a product from the list of registered products.

    Also uninstalls its .tie file handler and installs that of another product
    if appropriate."""
    if cxroot is None:
        cxroot = cxutils.CX_ROOT
    else:
        cxroot = os.path.abspath(cxroot)

    config = crossover.get_cross_product_config()

    if cxroot != cxutils.CX_ROOT:
        # Try cxtie first, if this looks like a non-legacy version
        product_key = _get_product_key(config, cxroot)
        if product_key is not None and \
                config[product_key].get('TieVersion', '0') != '0':
            cxtie = os.path.join(cxroot, 'bin', 'cxtie')
            if cxutils.system((cxtie, '--unregister')) == 0:
                return 0

    wconfig = config.get_save_config()
    wconfig.lock_file()

    product_key = _get_product_key(config, cxroot)
    if product_key is None:
        cxlog.warn('this product has already been unregistered')
        return 0
    installed = config[product_key].get('TieInstalled', 'false') == 'true'
    del wconfig[product_key]

    if cxroot == cxutils.CX_ROOT and installed:
        uninstall()
        best_tie = _get_best_tie(config)
        if best_tie and best_tie in wconfig:
            if _run_action(config[best_tie], 'TieInstall') == 0:
                wconfig[best_tie]['TieInstalled'] = 'true'

    # Unlock the file last to avoid races in the install/uninstall steps
    wconfig.save_and_unlock_file(True)


#
# Tie file handling
#


def _get_appname(c4pfile):
    autorun = c4pfile.get_autorun_id()
    for profile in c4pfile.profiles:
        if profile.appid == autorun:
            return profile.name
    return "'%s'" % c4pfile.filename

def open_file(filename):
    # pylint: disable-msg=W0404
    try:
        c4pfile = c4parser.C4PFile(filename, False, c4profiles.download)
    except Exception, e:
        # FIXME: C4PFile.__init__ doesn't actually raise an exception when
        # parsing fails for any reason, so we can't present a proper error to
        # the user.
        import tiehelpergui
        return tiehelpergui.show_invalid_file_error(filename, e)

    if c4pfile is None or not c4pfile.profiles:
        import tiehelpergui
        return tiehelpergui.show_invalid_file_error(filename, None)
    
    if c4pfile.malware_appid:
        import tiehelpergui
        return tiehelpergui.show_malware_dialog(c4pfile)

    products = c4pfile.find_products()
    
    if len(products) == 0:
        import tiehelpergui
        products = c4pfile.get_supported_product_ids()
        if len(products) == 0:
            return tiehelpergui.show_invalid_file_error(filename, None)
        else:
            return tiehelpergui.show_otherproduct_dialog(products, filename, _get_appname(c4pfile))
    elif len(products) == 1:
        product = products[0]
        cxinstaller = os.path.join(product['root'], 'bin', 'cxinstaller')
        os.execl(cxinstaller, cxinstaller, '--c4pfile', filename)
        # Unreachable code
        return 13
    else:
        import tiehelpergui
        return tiehelpergui.run(products, filename, _get_appname(c4pfile))


#
# Main
#

def main():
    # Parse the command line options
    opt_parser = cxopt.Parser(usage="%prog --register | --unregister | --install | --uninstall | --open TIEFILE | --help",
                              description="Finds and runs the appropriate CrossOver product to run a .tie file.")
    opt_parser.add_option('--register', action='store_true', dest='register', help="Registers this product and installs its tie handler if needed.")
    opt_parser.add_option('--unregister', action='store_true', dest='unregister', help="Unregisters this product and restores another tie handler if appropriate.")
    opt_parser.add_option('--cxroot', dest='cxroot', help="Specifies a different installed product to register or unregister.")
    opt_parser.add_option('--install', action='store_true', dest='install', help="Install the .tie associations for this program.")
    opt_parser.add_option('--uninstall', action='store_true', dest='uninstall', help="Remove the .tie associations for this program.")
    opt_parser.add_option('--open', dest='open_file', help="Open the given .tie file.")
    (options, args) = opt_parser.parse_args()

    # Do more checks and provide defaults
    if not options.register and not options.unregister and \
            not options.install and not options.uninstall and \
            not options.open_file:
        opt_parser.error("You must specify an action, such as --open")
    if sum(x and 1 or 0 for x in (options.register, options.unregister, options.install, options.uninstall, options.open_file)) >= 2:
        opt_parser.error("You must specify exactly one action")
    if args:
        opt_parser.error("unexpected argument '%s'" % args[0])
    if options.cxroot is not None and not options.register and not options.unregister:
        opt_parser.error("--cxroot must be combined with --register or --unregister")

    if options.register:
        return register(options.cxroot)
    elif options.unregister:
        return unregister(options.cxroot)
    elif options.install:
        return install()
    elif options.uninstall:
        return uninstall()
    elif options.open_file:
        return open_file(options.open_file)

if __name__ == "__main__":
    sys.exit(main())
