from __future__ import print_function

import json
import md5
import subprocess

import func_bug_reducer

import list_reducer
from list_reducer import TESTRESULT_KEEPPREFIX
from list_reducer import TESTRESULT_KEEPSUFFIX
from list_reducer import TESTRESULT_NOFAILURE

import swift_tools


class ReduceMiscompilingPasses(list_reducer.ListReducer):

    def __init__(self, lst, invoker):
        list_reducer.ListReducer.__init__(self, lst)
        self.invoker = invoker

    def run_test(self, prefix, suffix):
        # First, run the program with just the Suffix passes.  If it is still
        # broken with JUST the kept passes, discard the prefix passes.
        suffix_joined = ' '.join(suffix)
        suffix_hash = md5.md5(suffix_joined).hexdigest()
        print("Checking to see if suffix '%s' compiles correctly" %
              suffix_joined)

        result = self.invoker.invoke_with_passlist(
            suffix,
            self.invoker.get_suffixed_filename(suffix_hash))

        # Found a miscompile! Keep the suffix
        if result['exit_code'] != 0:
            print("Suffix crashes! Returning suffix")
            return (TESTRESULT_KEEPSUFFIX, prefix, suffix)

        if len(prefix) == 0:
            print("Suffix passes and no prefix passes, returning nofailure")
            return (TESTRESULT_NOFAILURE, prefix, suffix)

        print("Suffix '' does not crash! Current reduced program compiles "
              "without optimization!")

        # Next see if the program is broken if we run the "prefix" passes
        # first, then separately run the "kept" passes.
        prefix_joined = ' '.join(prefix)
        prefix_hash = md5.md5(prefix_joined).hexdigest()
        print("Checking to see if '%s' compiles correctly" % prefix_joined)

        # If it is not broken with the kept passes, it's possible that the
        # prefix passes must be run before the kept passes to break it.  If
        # the program WORKS after the prefix passes, but then fails if running
        # the prefix AND kept passes, we can update our bitcode file to
        # include the result of the prefix passes, then discard the prefix
        # passes.
        prefix_path = self.invoker.get_suffixed_filename(prefix_hash)
        result = self.invoker.invoke_with_passlist(
            prefix,
            prefix_path)
        if result['exit_code'] != 0:
            print("Prefix crashes the input by itself. Returning keep "
                  "prefix")
            return (TESTRESULT_KEEPPREFIX, prefix, suffix)

        # Ok, so now we know that the prefix passes work, first check if we
        # actually have any suffix passes. If we don't, just return.
        if len(suffix) == 0:
            print("No suffix, and prefix passes, returning no failure")
            return (TESTRESULT_NOFAILURE, prefix, suffix)

        # Otherwise, treat the prefix as our new baseline and see if suffix on
        # the new baseline finds the crash.
        original_input_file = self.invoker.input_file
        self.invoker.input_file = prefix_path
        print("Checking to see if '%s' compiles correctly after the '%s' "
              "passes" % (suffix_joined, prefix_joined))
        result = self.invoker.invoke_with_passlist(
            suffix,
            self.invoker.get_suffixed_filename(suffix_hash))

        # If we failed at this point, then the prefix is our new
        # baseline. Return keep suffix.
        if result['exit_code'] != 0:
            print("Suffix failed. Keeping prefix as new baseline")
            return (TESTRESULT_KEEPSUFFIX, prefix, suffix)

        # Otherwise, we must not be running the bad pass anymore. Restore the
        # original input_file and return NoFailure.
        self.invoker.input_file = original_input_file
        return (TESTRESULT_NOFAILURE, prefix, suffix)


def pass_bug_reducer(tools, config, passes, sil_opt_invoker, reduce_sil):
    # Make sure that the base case /does/ crash.
    filename = sil_opt_invoker.get_suffixed_filename('base_case')
    result = sil_opt_invoker.invoke_with_passlist(passes, filename)
    # If we succeed, there is no further work to do.
    if result['exit_code'] == 0:
        print("Does not crash on input passes!")
        print("Base Case: {}. Passes: {}".format(filename, ' '.join(passes)))
        return True
    print("Crashes with initial pass list! First trying to reduce the pass "
          "list!")

    # Otherwise, reduce the list of passes that cause the optimizer to crash.
    r = ReduceMiscompilingPasses(passes, sil_opt_invoker)
    if not r.reduce_list():
        print("Failed to find miscompiling pass list!")

    cmdline = sil_opt_invoker.cmdline_with_passlist(r.target_list)
    print("*** Found miscompiling passes!")
    print("*** Final File: %s" % sil_opt_invoker.input_file)
    print("*** Final Passes: %s" % (' '.join(r.target_list)))
    print("*** Repro command line: %s" % (' '.join(cmdline)))
    if not reduce_sil:
        return False

    print("*** User requested that we try to reduce SIL. Lets try.")
    input_file = sil_opt_invoker.input_file
    nm = swift_tools.SILNMInvoker(config, tools)
    sil_extract_invoker = swift_tools.SILFuncExtractorInvoker(config,
                                                              tools,
                                                              input_file)

    func_bug_reducer.function_bug_reducer(input_file, nm, sil_opt_invoker,
                                          sil_extract_invoker,
                                          r.target_list)
    print("*** Final Passes: %s" % (' '.join(r.target_list)))
    return False


def invoke_pass_bug_reducer(args):
    """Given a path to a sib file with canonical sil, attempt to find a
       perturbed list of passes that the perf pipeline"""
    tools = swift_tools.SwiftTools(args.swift_build_dir)
    config = swift_tools.SILToolInvokerConfig(args)

    passes = []
    if args.pass_list is None:
        json_data = json.loads(subprocess.check_output(
            [tools.sil_passpipeline_dumper, '-Performance']))
        passes = sum((p[1:] for p in json_data), [])
        passes = ['-' + x[1] for x in passes]
    else:
        passes = ['-' + x for x in args.pass_list]

    extra_args = []
    if args.extra_args is not None:
        extra_args = args.extra_args
    sil_opt_invoker = swift_tools.SILOptInvoker(config, tools,
                                                args.input_file,
                                                extra_args)
    pass_bug_reducer(tools, config, passes, sil_opt_invoker, args.reduce_sil)


def add_parser_arguments(parser):
    """Add parser arguments for opt_bug_reducer"""
    parser.set_defaults(func=invoke_pass_bug_reducer)
    parser.add_argument('input_file', help='The input file to optimize')
    parser.add_argument('--module-cache', help='The module cache to use')
    parser.add_argument('--sdk', help='The sdk to pass to sil-opt')
    parser.add_argument('--target', help='The target to pass to sil-opt')
    parser.add_argument('--resource-dir',
                        help='The resource-dir to pass to sil-opt')
    parser.add_argument('--work-dir',
                        help='Working directory to use for temp files',
                        default='bug_reducer')
    parser.add_argument('--module-name',
                        help='The name of the module we are optimizing')
    parser.add_argument('--pass', help='pass to test', dest='pass_list',
                        action='append')
    parser.add_argument('--extra-arg',
                        help='extra argument to pass to sil-opt',
                        dest='extra_args', action='append')
    parser.add_argument('--reduce-sil', help='After finding the relevant '
                        'passes, try to reduce the SIL by eliminating '
                        'functions, blocks, etc',
                        action='store_true')
