#!/usr/bin/env python3
# (c) Copyright 2017-2021. 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=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 cxopt
import cxdiag
import cxfixes

def detect_errors(options):
    # Collect the cxdiag issues. Do so even for --all so issues for which
    # there is no fix are reported at the end.
    cxdiag_flags = cxdiag.CHECK_PKGDEB | cxdiag.CHECK_PKGRPM
    if options.bits32:
        cxdiag_flags |= cxdiag.CHECK_32BIT
    if options.bits64:
        cxdiag_flags |= cxdiag.CHECK_64BIT
    diag = cxdiag.get(None, cxdiag_flags)
    for errid, title in diag.warnings.items():
        level = cxfixes.get_error_level(errid)
        if (options.required and level == 'required') or \
           (options.recommended and level == 'recommended') or \
           (options.suggested and level == 'suggested'):
            lib = diag.libs.get(errid, None)
            cxfixes.add_error(errid, title, level, lib)

def add_extra_errors(args, options):
    if not options.extra:
        return
    levels = set()
    if options.required:
        levels.add('required')
    if options.recommended:
        levels.add('recommended')
    if options.suggested:
        levels.add('suggested')
    if args:
        for errid in cxfixes.get_errors():
            level = cxfixes.get_error_level(errid)
            if level:
                levels.add(level)
        if 'suggested' in levels:
            levels.add('recommended')
        if 'recommended' in levels:
            levels.add('required')
    cxfixes.add_masked_errors(levels)

def rescan_errors(args, options):
    # We cannot rescan for errors if they were specified by the user
    # (either explicitly or --all)
    if not args and not options.all:
        cxfixes.clear_errors()
        detect_errors(options)
        add_extra_errors(args, options)

def main():
    opt_parser = cxopt.Parser(usage="%prog [--show|--show-all] [--required] [--recommended] [--suggested] [--32] [--64] [--no-extra] [--yes] [--dry-run] [--help] (--auto|--all|ISSUE1...)",
                              description="Shows or applies the fixes for the specified cxdiag issues. The default is: --auto --32 --64")
    opt_parser.add_option('--auto', action="store_true", help="Call cxdiag and apply the required and recommended fixes.")
    opt_parser.add_option('--required', action="store_true", help="Call cxdiag and apply the required fixes.")
    opt_parser.add_option('--recommended', action="store_true", help="Call cxdiag and apply the recommended fixes.")
    opt_parser.add_option('--suggested', action="store_true", help="Call cxdiag and apply the suggested fixes.")
    opt_parser.add_option('--32', action="store_true", dest="bits32", help="Fix the 32 bit and architecture-independent issues.")
    opt_parser.add_option('--64', action="store_true", dest="bits64", help="Fix the 64 bit issues.")
    opt_parser.add_option('--no-extra', action="store_false", default=True, dest="extra", help="Do not fix issues masked by missing libraries. For instance if the 32-bit C library is missing, do not install the 32-bit X11 library.")
    opt_parser.add_option('--show', action="store_true", help="Just show how the issues would be fixed for your distribution.")
    opt_parser.add_option('--show-all', action="store_true", help="Just show how the issues would be fixed for each supported distribution.")
    opt_parser.add_option('--yes', '-y', action="store_true", dest="yes", help="Tell the package installation tools to not ask for confirmation.")
    opt_parser.add_option('--verbose', action="store_true", help="Show the actions in more detail.")
    opt_parser.add_option('--dry-run', action="store_true", help="Show the command that would be run to fix the issues instead of running it.")
    opt_parser.add_option('--all', action="store_true", help="Apply all known fixes; even for issues not reported by cxdiag. This is only meant as a debugging tool.")
    (options, args) = opt_parser.parse_args()

    # Check the options
    if options.all and (options.auto or options.required or \
                        options.recommended or options.suggested):
        opt_parser.error("--all is incompatible with --auto, --required and --recommended")
    if options.auto or options.all or \
       (not options.required and not options.recommended and \
        not options.suggested and not args):
        options.required = options.recommended = True
    if not options.bits32 and not options.bits64:
        options.bits32 = options.bits64 = True

    if args:
        if options.required or options.recommended:
            opt_parser.error("unexpected argument '%s'" % args[0])
        for errid in args:
            if errid.startswith('-'):
                opt_parser.error("unknown option '%s'" % errid)
            cxfixes.add_error(errid.lower())
    else:
        detect_errors(options)
    add_extra_errors(args, options)

    # Show which distribution we are running on for reference.
    distro = cxfixes.detect_distribution()
    bitness = cxfixes.detect_bitness()
    print("Distribution: %s %s-bit (%s)" % (cxfixes.get_distribution_property(distro, 'name'), bitness, distro))
    cxid = cxfixes.detect_cxproduct()
    print("Product: %s" % cxid)

    if options.all:
        for errid, fix in cxfixes.CXFIXES.fixes.items():
            if errid.endswith('.amd64'):
                if not options.bits64:
                    continue
            else:
                if not options.bits32:
                    continue
            # Placeholder fixes are only there to provide title, description
            # and severity levels for the issues. They are not real fixes so
            # there is no point trying them and then reporting they cannot be
            # fixed, especially since most are of the 'brokenlib' form and will
            # point the user at the matching 'missinglib' page.
            # HACK: A lot of placeholders have a non-fix for the 'dummy'
            #        distribution for compatibility with CrossOver <= 19.
            if not fix.distfixes or \
                (len(fix.distfixes) == 1 and 'dummy' not in fix.distfixes):
                continue
            cxfixes.add_error(errid)

    if not cxfixes.has_errors():
        print("There is no issue to fix")
        return 0

    # Try to fix the issues all at once.
    rc = 0
    initial_count = len(cxfixes.get_errors())
    if not options.show and not options.show_all:
        if options.dry_run:
            fixable_errors, cmd = cxfixes.get_fix_command()
            print("\nYou may fix the issues by running the following command as root:")
            print(cmd)
            for errid in fixable_errors:
                cxfixes.remove_error(errid)
        else:
            rc = cxfixes.fix_errors(gui=False, new_console=False, yes=options.yes)
            if rc is None:
                # There was no fixable error. Still consider this success.
                rc = 0
            elif rc < 0:
                return 0 # canceled by the user
            else:
                # Some packages may have been installed so the cxdiag cache is
                # stale.
                cxdiag.clear()

    # If we tried to install a nonexistent package the above will have failed.
    # In that case try the fixes one by one.
    if not options.show and not options.show_all and not options.dry_run and \
       cxfixes.has_errors() and initial_count > 1:
        # Rescan for errors in case the previous command did fix some errors.
        # In particular apt-get sometimes returns an error despite having
        # successfully installed all packages. This may help avoid a second
        # attempt or at least reduce the number of packages to try to install.
        rescan_errors(args, options)

        # And only try to fix the remaining issues one by one if there are
        # **fixable** errors among them
        fixable_errors, _cmd = cxfixes.get_fix_command()
        if fixable_errors:
            print("")
            print("Trying to fix the issues one by one:")
            rc = cxfixes.fix_errors(new_console=False, update=False, allatonce=False, gui=False, yes=options.yes)
            if rc is None:
                rc = 0  # should not happen
            elif rc < 0:
                return 0 # canceled by the user
            else:
                # Some packages may have been installed so the cxdiag cache is
                # stale.
                cxdiag.clear()
            if cxfixes.has_errors():
                # Rescan again to only report unfixed errors
                rescan_errors(args, options)

    # Finally point the user to online help for issues we cannot fix.
    cxfixes.report_errors(prefix=False, gui=False, verbose=options.show_all or options.verbose)

    return rc


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