import errno
import logging
import os
import platform
import re
import sys
import textwrap
from pathlib import Path
from shutil import which

import numpy as np

from pytensor.configparser import (
    BoolParam,
    ConfigParam,
    EnumStr,
    FloatParam,
    IntParam,
    StrParam,
    _create_default_config,
)
from pytensor.utils import (
    LOCAL_BITWIDTH,
    PYTHON_INT_BITWIDTH,
    call_subprocess_Popen,
    maybe_add_to_os_environ_pathlist,
    output_subprocess_Popen,
)


_logger = logging.getLogger("pytensor.configdefaults")


def _filter_mode(val):
    # Do not add FAST_RUN_NOGC to this list (nor any other ALL CAPS shortcut).
    # The way to get FAST_RUN_NOGC is with the flag 'linker=c|py_nogc'.
    # The old all capital letter way of working is deprecated as it is not
    # scalable.
    str_options = [
        "Mode",
        "DebugMode",
        "FAST_RUN",
        "NanGuardMode",
        "FAST_COMPILE",
        "DEBUG_MODE",
        "CVM",
        "C",
        "JAX",
        "NUMBA",
        "PYTORCH",
        "MLX",
    ]
    if val in str_options:
        return val
    # This can be executed before PyTensor is completely imported, so
    # pytensor.compile.mode.Mode is not always available.
    # Instead of isinstance(val, pytensor.compile.mode.Mode),
    # we can inspect the __mro__ of the object!
    for type_ in type(val).__mro__:
        if "pytensor.compile.mode.Mode" in str(type_):
            return val
    raise ValueError(
        f"Expected one of {str_options}, or an instance of pytensor.compile.mode.Mode. "
        f"Instead got: {val}."
    )


def _is_valid_check_preallocated_output_param(param):
    if not isinstance(param, str):
        return False
    valid = [
        "initial",
        "previous",
        "c_contiguous",
        "f_contiguous",
        "strided",
        "wrong_size",
        "ALL",
        "",
    ]
    for p in param.split(":"):
        if p not in valid:
            return False
    return True


def _timeout_default():
    return config.compile__wait * 24


def _filter_vm_lazy(val):
    if val == "False" or val is False:
        return False
    elif val == "True" or val is True:
        return True
    elif val == "None" or val is None:
        return None
    else:
        raise ValueError(
            "Valid values for an vm__lazy parameter "
            f"should be None, False or True, not `{val}`."
        )


def short_platform(r=None, p=None):
    """
    Return a safe shorter version of platform.platform().

    The old default PyTensor compiledir used platform.platform in
    it. This use the platform.version() as a substring. This is too
    specific as it contain the full kernel number and package
    version. This cause the compiledir to change each time there is a
    new linux kernel update. This function remove the part of platform
    that are too precise.

    If we have something else then expected, we do nothing. So this
    should be safe on other OS.

    Some example if we use platform.platform() direction. On the same
    OS, with just some kernel updates.

    compiledir_Linux-2.6.32-504.el6.x86_64-x86_64-with-redhat-6.6-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-431.29.2.el6.x86_64-x86_64-with-redhat-6.5-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-431.23.3.el6.x86_64-x86_64-with-redhat-6.5-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-431.20.3.el6.x86_64-x86_64-with-redhat-6.5-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-431.17.1.el6.x86_64-x86_64-with-redhat-6.5-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-431.11.2.el6.x86_64-x86_64-with-redhat-6.5-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-431.el6.x86_64-x86_64-with-redhat-6.5-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-358.23.2.el6.x86_64-x86_64-with-redhat-6.4-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-358.6.2.el6.x86_64-x86_64-with-redhat-6.4-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-358.6.1.el6.x86_64-x86_64-with-redhat-6.4-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-358.2.1.el6.x86_64-x86_64-with-redhat-6.4-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-358.el6.x86_64-x86_64-with-redhat-6.4-Santiago-x86_64-2.6.6-64
    compiledir_Linux-2.6.32-358.el6.x86_64-x86_64-with-redhat-6.4-Santiago-x86_64-2.6.6
    compiledir_Linux-2.6.32-279.14.1.el6.x86_64-x86_64-with-redhat-6.4-Santiago-x86_64-2.6.6
    compiledir_Linux-2.6.32-279.14.1.el6.x86_64-x86_64-with-redhat-6.3-Santiago-x86_64-2.6.6
    compiledir_Linux-2.6.32-279.5.2.el6.x86_64-x86_64-with-redhat-6.3-Santiago-x86_64-2.6.6
    compiledir_Linux-2.6.32-220.13.1.el6.x86_64-x86_64-with-redhat-6.3-Santiago-x86_64-2.6.6
    compiledir_Linux-2.6.32-220.13.1.el6.x86_64-x86_64-with-redhat-6.2-Santiago-x86_64-2.6.6
    compiledir_Linux-2.6.32-220.7.1.el6.x86_64-x86_64-with-redhat-6.2-Santiago-x86_64-2.6.6
    compiledir_Linux-2.6.32-220.4.1.el6.x86_64-x86_64-with-redhat-6.2-Santiago-x86_64-2.6.6

    We suppose the version are ``X.Y[.*]-(digit)*(anything)*``. We keep ``X.Y``
    and don't keep less important digit in the part before ``-`` and we remove
    the leading digit after the first ``-``.

    If the information don't fit that pattern, we do not modify platform.

    """
    if r is None:
        r = platform.release()
    if p is None:
        p = platform.platform()
    sp = r.split("-")
    if len(sp) < 2:
        return p

    # For the split before the first -, we remove all learning digit:
    kernel_version = sp[0].split(".")
    if len(kernel_version) <= 2:
        # kernel version should always have at least 3 number.
        # If not, it use another semantic, so don't change it.
        return p
    sp[0] = ".".join(kernel_version[:2])

    # For the split after the first -, we remove leading non-digit value.
    rest = sp[1].split(".")
    while len(rest):
        if rest[0].isdigit():
            del rest[0]
        else:
            break
    sp[1] = ".".join(rest)

    # For sp[2:], we don't change anything.
    sr = "-".join(sp)
    p = p.replace(r, sr)

    return p


def add_basic_configvars():
    config.add(
        "floatX",
        "Default floating-point precision for python casts.\n"
        "\n"
        "Note: float16 support is experimental, use at your own risk.",
        EnumStr("float64", ["float32", "float16"]),
        # TODO: see gh-4466 for how to remove it.
        in_c_key=True,
    )

    config.add(
        "warn_float64",
        "Do an action when a tensor variable with float64 dtype is created.",
        EnumStr("ignore", ["warn", "raise", "pdb"]),
        in_c_key=False,
    )

    config.add(
        "cast_policy",
        "Rules for implicit type casting",
        EnumStr(
            "custom",
            ["numpy+floatX"],
            # The 'numpy' policy was originally planned to provide a
            # smooth transition from numpy. It was meant to behave the
            # same as numpy+floatX, but keeping float64 when numpy
            # would. However the current implementation of some cast
            # mechanisms makes it a bit more complex to add than what
            # was expected, so it is currently not available.
            # numpy,
        ),
        in_c_key=False,
    )

    config.add(
        "print_global_stats",
        "Print some global statistics (time spent) at the end",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "unpickle_function",
        (
            "Replace unpickled PyTensor functions with None. "
            "This is useful to unpickle old graphs that pickled"
            " them when it shouldn't"
        ),
        BoolParam(True),
        in_c_key=False,
    )


def _is_gt_0(x):
    return x > 0


def _is_ge_0(x):
    return x >= 0


def _is_valid_012(x):
    return x in (0, 1, 2)


def _default_cxx_bool():
    return bool(config.cxx)


def add_compile_configvars():
    config.add(
        "mode",
        "Default compilation mode",
        ConfigParam("Mode", apply=_filter_mode),
        in_c_key=False,
    )

    default_linker = "auto"

    linker_options = [
        "cvm",
        "c|py",
        "py",
        "c",
        "c|py_nogc",
        "vm",
        "vm_nogc",
        "cvm_nogc",
        "numba",
        "jax",
    ]

    config.add(
        "linker",
        "Default linker used if the pytensor flags mode is Mode or FAST_RUN",
        EnumStr(default_linker, linker_options, mutable=True),
        in_c_key=False,
    )

    # Keep the default value the same as the one for the mode FAST_RUN
    config.add(
        "allow_gc",
        "Do we default to delete intermediate results during PyTensor"
        " function calls? Doing so lowers the memory requirement, but"
        " asks that we reallocate memory at the next function call."
        " This is implemented for the default linker, but may not work"
        " for all linkers.",
        BoolParam(True),
        in_c_key=False,
    )

    # Keep the default optimizer the same as the one for the mode FAST_RUN
    config.add(
        "optimizer",
        "Default optimizer. If not None, will use this optimizer with the Mode",
        EnumStr(
            "o4",
            ["o3", "o2", "o1", "unsafe", "fast_run", "fast_compile", "merge", "None"],
            mutable=False,  # Not mutable because the default mode is cached after the first use.
        ),
        in_c_key=False,
    )

    config.add(
        "optimizer_verbose",
        "Print information about rewrites that are applied during a graph transformation.",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "optimizer_verbose_ignore",
        "Do not print information for rewrites with these names when `optimizer_verbose` is `True`. Separate names with ','",
        StrParam(""),
        in_c_key=False,
    )

    config.add(
        "compiler_verbose",
        "Print information about compilation steps.",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "on_opt_error",
        (
            "What to do when an optimization crashes: warn and skip it, raise "
            "the exception, or fall into the pdb debugger."
        ),
        EnumStr("warn", ["raise", "pdb", "ignore"]),
        in_c_key=False,
    )

    config.add(
        "on_unused_input",
        "What to do if a variable in the 'inputs' list of "
        " pytensor.function() is not used in the graph.",
        EnumStr("raise", ["warn", "ignore"]),
        in_c_key=False,
    )


def _get_cxx_default():
    param = "g++"

    # Test whether or not g++ is present: disable C code if it is not.
    try:
        rc = call_subprocess_Popen(["g++", "-v"])
    except OSError:
        rc = 1

    # Anaconda on Windows has mingw-w64 packages including GCC, but it may not be on PATH.
    if rc != 0:
        if sys.platform == "win32":
            mingw_w64_gcc = Path(sys.executable).parent / "Library/mingw-w64/bin/g++"
            try:
                rc = call_subprocess_Popen([str(mingw_w64_gcc), "-v"])
                if rc == 0:
                    maybe_add_to_os_environ_pathlist("PATH", mingw_w64_gcc.parent)
            except OSError:
                rc = 1
            if rc != 0:
                _logger.warning(
                    "g++ not available, if using conda: `conda install gxx`"
                )

    if rc != 0:
        param = ""

    # On Mac/FreeBSD we test for 'clang++' and use it by default
    if sys.platform == "darwin" or sys.platform.startswith("freebsd"):
        try:
            rc = call_subprocess_Popen(["clang++", "-v"])
            if rc == 0:
                param = "clang++"
        except OSError:
            pass

    # Try to find the full compiler path from the name
    if param != "":
        newp = which(param)
        if newp is not None:
            param = newp
        del newp

    # to support path that includes spaces, we need to wrap it with double quotes on Windows
    if param and os.name == "nt":
        param = f'"{param}"'

    return param


def _warn_cxx(val):
    """We only support clang++ as otherwise we hit strange g++/OSX bugs."""
    if sys.platform == "darwin" and val and "clang++" not in val:
        _logger.warning(
            "Only clang++ is supported. With g++, we end up with strange g++/OSX bugs."
        )
    return True


def _default_gcc_version_str():
    if config.cxx != "":
        try:
            p_out = output_subprocess_Popen([config.cxx, "-dumpversion"])
            gcc_version_str = p_out[0].strip().decode()
        except OSError:
            # Typically means gcc cannot be found.
            gcc_version_str = "GCC_NOT_FOUND"
    else:
        gcc_version_str = "GCC_NOT_FOUND"
    return gcc_version_str


def add_c_compile_configvars():
    config.add(
        "cxx",
        "The C++ compiler to use. Currently only g++ is"
        " supported, but supporting additional compilers should not be "
        "too difficult. "
        "If it is empty, no C++ code is compiled.",
        StrParam(_get_cxx_default, validate=_warn_cxx),
        in_c_key=False,
    )

    config.add(
        "gcc_version_str",
        "",
        StrParam(_default_gcc_version_str, mutable=False),
        in_c_key=False,
    )

    config.add(
        "gcc__cxxflags",
        "Extra compiler flags for gcc",
        StrParam(""),
        # Added elsewhere in the c key only when needed.
        in_c_key=False,
    )

    config.add(
        "nocleanup",
        "Suppress the deletion of code files that did not compile cleanly",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "cmodule__warn_no_version",
        "If True, will print a warning when compiling one or more Op "
        "with C code that can't be cached because there is no "
        "c_code_cache_version() function associated to at least one of "
        "those Ops.",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "cmodule__remove_gxx_opt",
        "If True, will remove the -O* parameter passed to g++."
        "This is useful to debug in gdb modules compiled by PyTensor."
        "The parameter -g is passed by default to g++",
        BoolParam(False),
        # TODO: change so that this isn't needed.
        # This can be done by handing this in compile_args()
        in_c_key=True,
    )

    config.add(
        "cmodule__compilation_warning",
        "If True, will print compilation warnings.",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "cmodule__preload_cache",
        "If set to True, will preload the C module cache at import time",
        BoolParam(False, mutable=False),
        in_c_key=False,
    )

    config.add(
        "cmodule__age_thresh_use",
        "In seconds. The time after which PyTensor won't reuse a compile c module.",
        # 24 days
        IntParam(60 * 60 * 24 * 24, mutable=False),
        in_c_key=False,
    )

    config.add(
        "cmodule__debug",
        "If True, define a DEBUG macro (if not exists) for any compiled C code.",
        BoolParam(False),
        in_c_key=True,
    )

    config.add(
        "compile__wait",
        """Time to wait before retrying to acquire the compile lock.""",
        IntParam(5, validate=_is_gt_0, mutable=False),
        in_c_key=False,
    )

    config.add(
        "compile__timeout",
        """In seconds, time that a process will wait before deciding to
    override an existing lock. An override only happens when the existing
    lock is held by the same owner *and* has not been 'refreshed' by this
    owner for more than this period. Refreshes are done every half timeout
    period for running processes.""",
        IntParam(_timeout_default, validate=_is_ge_0, mutable=False),
        in_c_key=False,
    )


def add_tensor_configvars():
    # This flag is used when we import PyTensor to initialize global variables.
    # So changing it after import will not modify these global variables.
    # This could be done differently... but for now we simply prevent it from being
    # changed at runtime.
    config.add(
        "tensor__cmp_sloppy",
        "Relax pytensor.tensor.math._allclose (0) not at all, (1) a bit, (2) more",
        IntParam(0, validate=_is_valid_012, mutable=False),
        in_c_key=False,
    )

    # http://developer.amd.com/CPU/LIBRARIES/LIBM/Pages/default.aspx
    config.add(
        "lib__amdlibm",
        "Use amd's amdlibm numerical library",
        BoolParam(False),
        # Added elsewhere in the c key only when needed.
        in_c_key=False,
    )

    config.add(
        "tensor__insert_inplace_optimizer_validate_nb",
        "-1: auto, if graph have less then 500 nodes 1, else 10",
        IntParam(-1),
        in_c_key=False,
    )


def add_traceback_configvars():
    config.add(
        "traceback__limit",
        "The number of stack to trace. -1 mean all.",
        # We default to a number to be able to know where v1 + v2 is created in the
        # user script. The bigger this number is, the more run time it takes.
        # We need to default to 8 to support pytensor.tensor.type.tensor(...).
        # import pytensor, numpy
        # X = pytensor.tensor.matrix()
        # y = X.reshape((5,3,1))
        # assert y.tag.trace
        IntParam(8),
        in_c_key=False,
    )

    config.add(
        "traceback__compile_limit",
        "The number of stack to trace to keep during compilation. -1 mean all."
        " If greater then 0, will also make us save PyTensor internal stack trace.",
        IntParam(0),
        in_c_key=False,
    )


def add_error_and_warning_configvars():
    # Note to developers:
    # Generally your exceptions should use an apply node's __str__
    # method when exception_verbosity == 'low'. When exception_verbosity
    # == 'high', you should include a call to printing.min_informative_str
    # on all important apply nodes.
    config.add(
        "exception_verbosity",
        "Verbosity of exceptions generated by PyTensor functions.",
        EnumStr("low", ["medium", "high"]),
        in_c_key=False,
    )


def add_testvalue_and_checking_configvars():
    config.add(
        "check_input",
        "Specify if types should check their input in their C code. "
        "It can be used to speed up compilation, reduce overhead "
        "(particularly for scalars) and reduce the number of generated C "
        "files.",
        BoolParam(True),
        in_c_key=True,
    )
    config.add(
        "NanGuardMode__nan_is_error",
        "Default value for nan_is_error",
        BoolParam(True),
        in_c_key=False,
    )

    config.add(
        "NanGuardMode__inf_is_error",
        "Default value for inf_is_error",
        BoolParam(True),
        in_c_key=False,
    )

    config.add(
        "NanGuardMode__big_is_error",
        "Default value for big_is_error",
        BoolParam(True),
        in_c_key=False,
    )

    config.add(
        "NanGuardMode__action",
        "What NanGuardMode does when it finds a problem",
        EnumStr("raise", ["warn", "pdb"]),
        in_c_key=False,
    )

    config.add(
        "DebugMode__patience",
        "Optimize graph this many times to detect inconsistency",
        IntParam(10, _is_gt_0),
        in_c_key=False,
    )

    config.add(
        "DebugMode__check_c",
        "Run C implementations where possible",
        BoolParam(_default_cxx_bool),
        in_c_key=False,
    )

    config.add(
        "DebugMode__check_py",
        "Run Python implementations where possible",
        BoolParam(True),
        in_c_key=False,
    )

    config.add(
        "DebugMode__check_finite",
        "True -> complain about NaN/Inf results",
        BoolParam(True),
        in_c_key=False,
    )

    config.add(
        "DebugMode__check_strides",
        (
            "Check that Python- and C-produced ndarrays have same strides. "
            "On difference: (0) - ignore, (1) warn, or (2) raise error"
        ),
        # TODO: make this an Enum setting
        IntParam(0, validate=_is_valid_012),
        in_c_key=False,
    )

    config.add(
        "DebugMode__warn_input_not_reused",
        (
            "Generate a warning when destroy_map or view_map says that an "
            "op works inplace, but the op did not reuse the input for its "
            "output."
        ),
        BoolParam(True),
        in_c_key=False,
    )

    config.add(
        "DebugMode__check_preallocated_output",
        (
            "Test thunks with pre-allocated memory as output storage. "
            'This is a list of strings separated by ":". Valid values are: '
            '"initial" (initial storage in storage map, happens with Scan),'
            '"previous" (previously-returned memory), '
            '"c_contiguous", "f_contiguous", '
            '"strided" (positive and negative strides), '
            '"wrong_size" (larger and smaller dimensions), and '
            '"ALL" (all of the above).'
        ),
        StrParam("", validate=_is_valid_check_preallocated_output_param),
        in_c_key=False,
    )

    config.add(
        "DebugMode__check_preallocated_output_ndim",
        (
            'When testing with "strided" preallocated output memory, '
            "test all combinations of strides over that number of "
            "(inner-most) dimensions. You may want to reduce that number "
            "to reduce memory or time usage, but it is advised to keep a "
            "minimum of 2."
        ),
        IntParam(4, _is_gt_0),
        in_c_key=False,
    )

    config.add(
        "profiling__time_thunks",
        """Time individual thunks when profiling""",
        BoolParam(True),
        in_c_key=False,
    )

    config.add(
        "profiling__n_apply",
        "Number of Apply instances to print by default",
        IntParam(20, _is_gt_0),
        in_c_key=False,
    )

    config.add(
        "profiling__n_ops",
        "Number of Ops to print by default",
        IntParam(20, _is_gt_0),
        in_c_key=False,
    )

    config.add(
        "profiling__output_line_width",
        "Max line width for the profiling output",
        IntParam(512, _is_gt_0),
        in_c_key=False,
    )

    config.add(
        "profiling__min_memory_size",
        """For the memory profile, do not print Apply nodes if the size
                 of their outputs (in bytes) is lower than this threshold""",
        IntParam(1024, validate=_is_ge_0),
        in_c_key=False,
    )

    config.add(
        "profiling__min_peak_memory",
        """The min peak memory usage of the order""",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "profiling__destination",
        """File destination of the profiling output""",
        StrParam("stderr"),
        in_c_key=False,
    )

    config.add(
        "profiling__debugprint",
        """Do a debugprint of the profiled functions""",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "profiling__ignore_first_call",
        """Do we ignore the first call of an PyTensor function.""",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "on_shape_error",
        "warn: print a warning and use the default value. raise: raise an error",
        EnumStr("warn", ["raise"]),
        in_c_key=False,
    )


def add_multiprocessing_configvars():
    # Test if the env variable is set
    var = os.getenv("OMP_NUM_THREADS", None)
    if var:
        try:
            int(var)
        except ValueError:
            raise TypeError(
                f"The environment variable OMP_NUM_THREADS should be a number, got '{var}'."
            )
        else:
            default_openmp = int(var) != 1
    else:
        # Check the number of cores availables.
        count = os.cpu_count()
        if count is None:
            _logger.warning(
                "We are not able to detect the number of CPU cores."
                " We disable openmp by default. To remove this"
                " warning, set the environment variable"
                " OMP_NUM_THREADS to the number of threads you"
                " want pytensor to use."
            )
        default_openmp = count > 1

    # Disable it by default for now as currently only the ConvOp supports
    # it, and this causes slowdown by default as we do not disable it for
    # too small convolution.
    default_openmp = False

    config.add(
        "openmp",
        "Allow (or not) parallel computation on the CPU with OpenMP. "
        "This is the default value used when creating an Op that "
        "supports OpenMP parallelization. It is preferable to define it "
        "via the PyTensor configuration file ~/.pytensorrc or with the "
        "environment variable PYTENSOR_FLAGS. Parallelization is only "
        "done for some operations that implement it, and even for "
        "operations that implement parallelism, each operation is free "
        "to respect this flag or not. You can control the number of "
        "threads used with the environment variable OMP_NUM_THREADS."
        " If it is set to 1, we disable openmp in PyTensor by default.",
        BoolParam(default_openmp),
        in_c_key=False,
    )

    config.add(
        "openmp_elemwise_minsize",
        "If OpenMP is enabled, this is the minimum size of vectors "
        "for which the openmp parallelization is enabled "
        "in element wise ops.",
        IntParam(200000),
        in_c_key=False,
    )


def add_optimizer_configvars():
    config.add(
        "optimizer_excluding",
        (
            "When using the default mode, we will remove optimizer with "
            "these tags. Separate tags with ':'."
        ),
        StrParam("", mutable=False),
        in_c_key=False,
    )

    config.add(
        "optimizer_including",
        (
            "When using the default mode, we will add optimizer with "
            "these tags. Separate tags with ':'."
        ),
        StrParam("", mutable=False),
        in_c_key=False,
    )

    config.add(
        "optimizer_requiring",
        (
            "When using the default mode, we will require optimizer with "
            "these tags. Separate tags with ':'."
        ),
        StrParam("", mutable=False),
        in_c_key=False,
    )

    config.add(
        "optdb__position_cutoff",
        "Where to stop earlier during optimization. It represent the"
        " position of the optimizer where to stop.",
        FloatParam(np.inf),
        in_c_key=False,
    )

    config.add(
        "optdb__max_use_ratio",
        "A ratio that prevent infinite loop in EquilibriumGraphRewriter.",
        FloatParam(8),
        in_c_key=False,
    )
    config.add(
        "cycle_detection",
        "If cycle_detection is set to regular, most inplaces are allowed,"
        "but it is slower. If cycle_detection is set to faster, less inplaces"
        "are allowed, but it makes the compilation faster."
        "The interaction of which one give the lower peak memory usage is"
        "complicated and not predictable, so if you are close to the peak"
        "memory usage, trying both could give you a small gain.",
        EnumStr("regular", ["fast"]),
        in_c_key=False,
    )

    config.add(
        "check_stack_trace",
        "A flag for checking the stack trace during the optimization process. "
        "default (off): does not check the stack trace of any optimization "
        "log: inserts a dummy stack trace that identifies the optimization"
        "that inserted the variable that had an empty stack trace."
        "warn: prints a warning if a stack trace is missing and also a dummy"
        "stack trace is inserted that indicates which optimization inserted"
        "the variable that had an empty stack trace."
        "raise: raises an exception if a stack trace is missing",
        EnumStr("off", ["log", "warn", "raise"]),
        in_c_key=False,
    )


def add_vm_configvars():
    config.add(
        "profile",
        "If VM should collect profile information",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "profile_optimizer",
        "If VM should collect optimizer profile information",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "profile_memory",
        "If VM should collect memory profile information and print it",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "vm__lazy",
        "Useful only for the VM Linkers. When lazy is None,"
        " auto detect if lazy evaluation is needed and use the appropriate"
        " version. If the C loop isn't being used and lazy is True, use "
        "the Stack VM; otherwise, use the Loop VM.",
        ConfigParam("None", apply=_filter_vm_lazy),
        in_c_key=False,
    )


def add_scan_configvars():
    config.add(
        "scan__allow_gc",
        "Allow/disallow gc inside of Scan (default: False)",
        BoolParam(False),
        in_c_key=False,
    )

    config.add(
        "scan__allow_output_prealloc",
        "Allow/disallow memory preallocation for outputs inside of scan "
        "(default: True)",
        # Non-mutable because ScanSaveMem rewrite checks it,
        # and we can't have the rewrite and the implementation mismatch
        BoolParam(True, mutable=False),
        in_c_key=False,
    )


def add_numba_configvars():
    config.add(
        "numba__fastmath",
        ("If True, use Numba's fastmath mode."),
        BoolParam(True),
        in_c_key=False,
    )
    config.add(
        "numba__cache",
        ("If True, use Numba's file based caching."),
        BoolParam(True),
        in_c_key=False,
    )


def _filter_base_compiledir(path: str | Path) -> Path:
    # Expand '~' in path
    return Path(path).expanduser()


def _filter_compiledir(path: str | Path) -> Path:
    # Expand '~' in path
    path = Path(path).expanduser()
    # Turn path into the 'real' path. This ensures that:
    #   1. There is no relative path, which would fail e.g. when trying to
    #      import modules from the compile dir.
    #   2. The path is stable w.r.t. e.g. symlinks (which makes it easier
    #      to re-use compiled modules).
    path = path.resolve()
    if path.exists():
        if not os.access(path, os.R_OK | os.W_OK | os.X_OK):
            # If it exist we need read, write and listing access
            raise ValueError(
                f"compiledir '{path}' exists but you don't have read, write"
                " or listing permissions."
            )
    else:
        try:
            # 0o770 = read-write-execute for user and group
            path.mkdir(mode=0o770, parents=True, exist_ok=True)
        except OSError as e:
            # Maybe another parallel execution of pytensor was trying to create
            # the same directory at the same time.
            if e.errno != errno.EEXIST:
                raise ValueError(
                    "Unable to create the compiledir directory"
                    f" '{path}'. Check the permissions."
                )

    # PROBLEM: sometimes the initial approach based on
    # os.system('touch') returned -1 for an unknown reason; the
    # alternate approach here worked in all cases... it was weird.
    # No error should happen as we checked the permissions.
    init_file = path / "__init__.py"
    if not init_file.exists():
        try:
            with init_file.open("w"):
                pass
        except OSError as e:
            if init_file.exists():
                pass  # has already been created
            else:
                e.args += (f"{path} exist? {path.exists()}",)
                raise
    return path


def _get_home_dir() -> Path:
    """
    Return location of the user's home directory.

    """
    if (env_home := os.getenv("HOME")) is not None:
        return Path(env_home)

    # This usually works on Windows (see discussion on theano-users, July 13 2010).
    path_home = Path.home()
    if str(path_home) != "~":
        return path_home

    # This might happen when expanduser fails. Although the cause of
    # failure is a mystery, it has been seen on some Windows system.
    windowsfail_home = os.getenv("USERPROFILE")
    assert windowsfail_home is not None
    return Path(windowsfail_home)


_compiledir_format_keys = {
    "platform",
    "short_platform",
    "processor",
    "python_version",
    "python_bitwidth",
    "python_int_bitwidth",
    "pytensor_version",
    "numpy_version",
    "gxx_version",
    "hostname",
}


def _default_compiledir() -> Path:
    from pytensor import __version__

    compiledir_dict = {
        "platform": platform.platform(),
        "short_platform": short_platform(),
        "processor": platform.processor(),
        "python_version": platform.python_version(),
        "python_bitwidth": LOCAL_BITWIDTH,
        "python_int_bitwidth": PYTHON_INT_BITWIDTH,
        "pytensor_version": __version__,
        "numpy_version": np.__version__,
        "gxx_version": (config.gcc_version_str.replace(" ", "_"),),
        "hostname": platform.node(),
    }
    assert set(compiledir_dict) == _compiledir_format_keys
    formatted = config.compiledir_format % compiledir_dict
    safe = re.sub(r"[\(\)\s,]+", "_", formatted)

    return config.base_compiledir / safe


_compiledir_format_keys_str = ", ".join(sorted(_compiledir_format_keys))
_default_compiledir_format = (
    "compiledir_%(short_platform)s-%(processor)s-%(python_version)s-%(python_bitwidth)s"
)


def add_caching_dir_configvars():
    config.add(
        "compiledir_format",
        textwrap.fill(
            textwrap.dedent(
                f"""\
                     Format string for platform-dependent compiled
                     module subdirectory (relative to base_compiledir).
                     Available keys: {_compiledir_format_keys_str}.
                 """
            )
        ),
        StrParam(_default_compiledir_format, mutable=False),
        in_c_key=False,
    )

    # On Windows we should avoid writing temporary files to a directory that is
    # part of the roaming part of the user profile. Instead we use the local part
    # of the user profile, when available.
    if sys.platform == "win32" and os.getenv("LOCALAPPDATA") is not None:
        default_base_compiledir = Path(os.getenv("LOCALAPPDATA")) / "PyTensor"
    else:
        default_base_compiledir = _get_home_dir() / ".pytensor"

    config.add(
        "base_compiledir",
        "platform-independent root directory for compiled modules",
        ConfigParam(
            default_base_compiledir, apply=_filter_base_compiledir, mutable=False
        ),
        in_c_key=False,
    )

    config.add(
        "compiledir",
        "platform-dependent cache directory for compiled modules",
        ConfigParam(_default_compiledir, apply=_filter_compiledir, mutable=False),
        in_c_key=False,
    )


# Eventually, the instance of `PyTensorConfigParser` should be created right here,
# where it is also populated with settings.
config = _create_default_config()

# The functions below register config variables into the config instance above.
add_basic_configvars()
add_compile_configvars()
add_c_compile_configvars()
add_tensor_configvars()
add_traceback_configvars()
add_error_and_warning_configvars()
add_testvalue_and_checking_configvars()
add_multiprocessing_configvars()
add_optimizer_configvars()
add_vm_configvars()
add_numba_configvars()
add_caching_dir_configvars()
