import os
import sys


class TestingConfig(object):
    """
    TestingConfig - Information on the tests inside a suite.
    """

    @staticmethod
    def fromdefaults(litConfig):
        """
        fromdefaults(litConfig) -> TestingConfig

        Create a TestingConfig object with default values.
        """
        # Set the environment based on the command line arguments.
        environment = {
            "PATH": os.pathsep.join(litConfig.path + [os.environ.get("PATH", "")]),
            "LLVM_DISABLE_CRASH_REPORT": "1",
        }

        pass_vars = [
            "LIBRARY_PATH",
            "LD_LIBRARY_PATH",
            "SYSTEMROOT",
            "TERM",
            "CLANG",
            "CLANG_TOOLCHAIN_PROGRAM_TIMEOUT",
            "LLDB",
            "LD_PRELOAD",
            "LLVM_SYMBOLIZER_PATH",
            "LLVM_PROFILE_FILE",
            "ASAN_SYMBOLIZER_PATH",
            "HWASAN_SYMBOLIZER_PATH",
            "LSAN_SYMBOLIZER_PATH",
            "MSAN_SYMBOLIZER_PATH",
            "TSAN_SYMBOLIZER_PATH",
            "UBSAN_SYMBOLIZER_PATH",
            "ASAN_OPTIONS",
            "LSAN_OPTIONS",
            "HWASAN_OPTIONS",
            "MSAN_OPTIONS",
            "RTSAN_OPTIONS",
            "TSAN_OPTIONS",
            "UBSAN_OPTIONS",
            "ADB",
            "ADB_SERVER_SOCKET",
            "ANDROID_SERIAL",
            "SSH_AUTH_SOCK",
            "SANITIZER_IGNORE_CVE_2016_2143",
            "TMPDIR",
            "TMP",
            "TEMP",
            "TEMPDIR",
            "AVRLIT_BOARD",
            "AVRLIT_PORT",
            "FILECHECK_OPTS",
            "VCINSTALLDIR",
            "VCToolsinstallDir",
            "VSINSTALLDIR",
            "WindowsSdkDir",
            "WindowsSDKLibVersion",
            "SOURCE_DATE_EPOCH",
            "GTEST_FILTER",
            "DFLTCC",
            "QEMU_LD_PREFIX",
            "QEMU_CPU",
            "HOME",
        ]

        if sys.platform.startswith("aix"):
            pass_vars += ["LIBPATH"]
        elif sys.platform == "win32":
            pass_vars += [
                "COMSPEC",
                "INCLUDE",
                "LIB",
                "PATHEXT",
                "USERPROFILE",
            ]
            environment["PYTHONBUFFERED"] = "1"
            # Avoid Windows heuristics which try to detect potential installer
            # programs (which may need to run with elevated privileges) and ask
            # if the user wants to run them in that way. This heuristic may
            # match for executables containing the substrings "patch" (which is
            # a substring of "dispatch"), "update", "setup", etc. Set this
            # environment variable indicating that we want to execute them with
            # the current user.
            environment["__COMPAT_LAYER"] = "RunAsInvoker"

        for var in pass_vars:
            val = os.environ.get(var, "")
            # Check for empty string as some variables such as LD_PRELOAD cannot be empty
            # ('') for OS's such as OpenBSD.
            if val:
                environment[var] = val

        # Set the default available features based on the LitConfig.
        available_features = []
        if litConfig.useValgrind:
            available_features.append("valgrind")
            if litConfig.valgrindLeakCheck:
                available_features.append("vg_leak")

        return TestingConfig(
            None,
            name="<unnamed>",
            suffixes=set(),
            test_format=None,
            environment=environment,
            substitutions=[],
            unsupported=False,
            test_exec_root=None,
            test_source_root=None,
            excludes=[],
            available_features=available_features,
            pipefail=True,
            standalone_tests=False,
        )

    def load_from_path(self, path, litConfig):
        """
        load_from_path(path, litConfig)

        Load the configuration module at the provided path into the given config
        object.
        """

        # Load the config script data.
        data = None
        f = open(path)
        try:
            data = f.read()
        except:
            litConfig.fatal("unable to load config file: %r" % (path,))
        f.close()

        # Execute the config script to initialize the object.
        cfg_globals = dict(globals())
        cfg_globals["config"] = self
        cfg_globals["lit_config"] = litConfig
        cfg_globals["__file__"] = path
        try:
            exec(compile(data, path, "exec"), cfg_globals, None)
            litConfig.dbg("... loaded config %r" % path)
        except SystemExit:
            e = sys.exc_info()[1]
            # We allow normal system exit inside a config file to just
            # return control without error.
            if e.args:
                raise
        except:
            import traceback

            litConfig.fatal(
                "unable to parse config file %r, traceback: %s"
                % (path, traceback.format_exc())
            )
        self.finish(litConfig)

    def __init__(
        self,
        parent,
        name,
        suffixes,
        test_format,
        environment,
        substitutions,
        unsupported,
        test_exec_root,
        test_source_root,
        excludes,
        available_features,
        pipefail,
        limit_to_features=[],
        is_early=False,
        parallelism_group=None,
        standalone_tests=False,
    ):
        self.parent = parent
        self.name = str(name)
        self.suffixes = set(suffixes)
        self.test_format = test_format
        self.environment = dict(environment)
        self.substitutions = list(substitutions)
        self.unsupported = unsupported
        self.test_exec_root = test_exec_root
        self.test_source_root = test_source_root
        self.excludes = set(excludes)
        self.available_features = set(available_features)
        self.pipefail = pipefail
        self.standalone_tests = standalone_tests
        # This list is used by TestRunner.py to restrict running only tests that
        # require one of the features in this list if this list is non-empty.
        # Configurations can set this list to restrict the set of tests to run.
        self.limit_to_features = set(limit_to_features)
        self.parallelism_group = parallelism_group
        self._recursiveExpansionLimit = None

    @property
    def recursiveExpansionLimit(self):
        return self._recursiveExpansionLimit

    @recursiveExpansionLimit.setter
    def recursiveExpansionLimit(self, value):
        if value is not None and not isinstance(value, int):
            raise ValueError(
                "recursiveExpansionLimit must be either None or an integer (got <{}>)".format(
                    value
                )
            )
        if isinstance(value, int) and value < 0:
            raise ValueError(
                "recursiveExpansionLimit must be a non-negative integer (got <{}>)".format(
                    value
                )
            )
        self._recursiveExpansionLimit = value

    def finish(self, litConfig):
        """finish() - Finish this config object, after loading is complete."""

        self.name = str(self.name)
        self.suffixes = set(self.suffixes)
        self.environment = dict(self.environment)
        self.substitutions = list(self.substitutions)
        if self.test_exec_root is not None:
            # FIXME: This should really only be suite in test suite config
            # files. Should we distinguish them?
            self.test_exec_root = str(self.test_exec_root)
        if self.test_source_root is not None:
            # FIXME: This should really only be suite in test suite config
            # files. Should we distinguish them?
            self.test_source_root = str(self.test_source_root)
        self.excludes = set(self.excludes)
        if (
            litConfig.maxRetriesPerTest is not None
            and getattr(self, "test_retry_attempts", None) is None
        ):
            self.test_retry_attempts = litConfig.maxRetriesPerTest

    @property
    def root(self):
        """root attribute - The root configuration for the test suite."""
        if self.parent is None:
            return self
        else:
            return self.parent.root


class SubstituteCaptures:
    """
    Helper class to indicate that the substitutions contains backreferences.

    This can be used as the following in lit.cfg to mark subsitutions as having
    back-references::

        config.substutions.append(('\b[^ ]*.cpp', SubstituteCaptures('\0.txt')))

    """

    def __init__(self, substitution):
        self.substitution = substitution

    def replace(self, pattern, replacement):
        return self.substitution

    def __str__(self):
        return self.substitution

    def __len__(self):
        return len(self.substitution)

    def __getitem__(self, item):
        return self.substitution.__getitem__(item)
