#     Copyright 2024, Kay Hayen, mailto:kay.hayen@gmail.com find license text at end of file


""" Module for constants in Nuitka.

This contains tools to compare, classify and test constants.
"""

import math
import sys
from types import BuiltinFunctionType

from nuitka.Builtins import builtin_type_names
from nuitka.PythonVersions import python_version

from .__past__ import GenericAlias, UnionType, iterItems, long, unicode, xrange
from .Builtins import (
    builtin_anon_names,
    builtin_anon_value_list,
    builtin_exception_values_list,
    builtin_named_values_list,
)

NoneType = type(None)

EllipsisType = type(Ellipsis)


def compareConstants(a, b):
    # Many many cases to deal with, pylint: disable=too-many-branches,too-many-return-statements

    # Supposed fast path for comparison.
    if type(a) is not type(b):
        return False

    # Now it's either not the same, or it is a container that contains NaN or it
    # is a complex or float that is NaN, the other cases can use == at the end.
    if type(a) is complex:
        return compareConstants(a.imag, b.imag) and compareConstants(a.real, b.real)

    if type(a) is float:
        # Check sign first, -0.0 is not 0.0, or -nan is not nan, it has a
        # different sign for a start.
        if math.copysign(1.0, a) != math.copysign(1.0, b):
            return False

        if math.isnan(a) and math.isnan(b):
            return True

        return a == b

    if type(a) in (tuple, list):
        if len(a) != len(b):
            return False

        for ea, eb in zip(a, b):
            if not compareConstants(ea, eb):
                return False
        return True

    if type(a) is dict:
        if len(a) != len(b):
            return False

        for ea1, ea2 in iterItems(a):
            for eb1, eb2 in iterItems(b):
                if compareConstants(ea1, eb1) and compareConstants(ea2, eb2):
                    break
            else:
                return False
        return True

    if type(a) in (frozenset, set):
        if len(a) != len(b):
            return False

        for ea in a:
            if ea not in b:
                # Due to NaN values, we need to compare each set element with
                # all the other set to be really sure.
                for eb in b:
                    if compareConstants(ea, eb):
                        break
                else:
                    return False
        return True

    if type(a) is xrange:
        return str(a) == str(b)

    # The NaN values of float and complex may let this fail, even if the
    # constants are built in the same way, therefore above checks.
    return a == b


# These built-in type references are kind of constant too. The list should be
# complete.
constant_builtin_types = (
    int,
    str,
    float,
    list,
    tuple,
    set,
    dict,
    slice,
    complex,
    xrange,
    NoneType,
)

if python_version >= 0x300:
    constant_builtin_types += (bytes,)
else:
    constant_builtin_types += (
        unicode,
        long,
        # This has no name in Python, but the natural one in C-API.
        builtin_anon_names["instance"],
    )


def isConstant(constant):
    # Too many cases and all return, that is how we do it here,
    # pylint: disable=too-many-branches,too-many-return-statements

    constant_type = type(constant)

    if constant_type is dict:
        for key, value in iterItems(constant):
            if not isConstant(key):
                return False
            if not isConstant(value):
                return False
        return True
    elif constant_type in (tuple, list):
        for element_value in constant:
            if not isConstant(element_value):
                return False
        return True
    elif constant_type is slice:
        if (
            not isConstant(constant.start)
            or not isConstant(constant.stop)
            or not isConstant(constant.step)
        ):
            return False

        return True
    elif constant_type in (
        str,
        unicode,
        complex,
        int,
        long,
        bool,
        float,
        NoneType,
        range,
        bytes,
        set,
        frozenset,
        xrange,
        bytearray,
        EllipsisType,
    ):
        return True
    elif constant in (NoneType, NotImplemented):
        return True
    elif constant in builtin_anon_value_list:
        return True
    elif constant_type is type:
        # Maybe pre-build this as a set for quicker testing.
        return (
            constant.__name__ in builtin_type_names
            or constant in builtin_exception_values_list
        )
    elif constant_type is BuiltinFunctionType and constant in builtin_named_values_list:
        # TODO: Some others could also be usable and even interesting, but
        # then probably should go into other node types, e.g. str.join is
        # a candidate.
        return True
    elif constant_type is GenericAlias:
        return True
    elif constant_type is UnionType:
        return True
    elif constant is sys.version_info:
        return True
    else:
        return False


def isMutable(constant):
    """Is a constant mutable

    That means a user of a reference to it, can modify it. Strings are
    a prime example of immutable, dictionaries are mutable.
    """
    # Many cases and all return, that is how we do it here,
    # pylint: disable=too-many-return-statements

    constant_type = type(constant)

    if constant_type in (
        str,
        unicode,
        complex,
        int,
        long,
        bool,
        float,
        NoneType,
        range,
        bytes,
        slice,
        xrange,
        type,
        frozenset,
        BuiltinFunctionType,
        EllipsisType,
    ):
        return False
    elif constant_type in (dict, list, set, bytearray):
        return True
    elif constant_type is tuple:
        for value in constant:
            if isMutable(value):
                return True
        return False
    elif constant is NotImplemented:
        return False
    elif constant_type is GenericAlias:
        return isMutable(constant.__origin__) or isMutable(constant.__args__)
    elif constant_type is UnionType:
        return False
    elif constant is sys.version_info:
        return False
    else:
        assert False, repr(constant)


def isHashable(constant):
    """Is a constant hashable

    That means a user of a reference to it, can use it for dicts and set
    keys. This is distinct from mutable, there is one types that is not
    mutable, and still not hashable: slices.
    """
    # Many cases and all return, that is how we do it here,

    constant_type = type(constant)

    if constant_type in (
        str,
        unicode,
        complex,
        int,
        long,
        bool,
        float,
        NoneType,
        xrange,
        bytes,
        type,
        frozenset,
        BuiltinFunctionType,
        EllipsisType,
    ):
        return True
    elif constant_type in (dict, list, set, slice, bytearray):
        return False
    elif constant_type is tuple:
        for value in constant:
            if not isHashable(value):
                return False
        return True
    else:
        assert False, constant_type


def getUnhashableConstant(constant):
    """Get one unhashable part of a constant."""

    constant_type = type(constant)

    if constant_type in (
        str,
        unicode,
        complex,
        int,
        long,
        bool,
        float,
        NoneType,
        xrange,
        bytes,
        type,
        slice,
        BuiltinFunctionType,
        EllipsisType,
    ):
        return None
    elif constant_type in (dict, list, set):
        return constant
    elif constant_type is tuple:
        for value in constant:
            res = getUnhashableConstant(value)
            if res is not None:
                return res
        return None
    elif constant in constant_builtin_types:
        return None
    else:
        assert False, constant_type


def createConstantDict(keys, values):
    # Create it proper size immediately.
    constant_value = dict.fromkeys(keys, None)

    for key, value in zip(keys, values):
        constant_value[key] = value

    return constant_value


def isCompileTimeConstantValue(value):
    """Determine if a value will be usable at compile time."""
    # This needs to match code in makeCompileTimeConstantReplacementNode
    if isConstant(value):
        return True
    elif type(value) is type:
        return True
    else:
        return False


# Shared empty values, it would cost time to create them locally.
the_empty_dict = {}
the_empty_list = []
the_empty_set = set()
the_empty_bytearray = bytearray()

the_empty_tuple = ()
the_empty_frozenset = frozenset()
the_empty_slice = slice(None)

the_empty_unicode = unicode()  # black doesn't let us write u"" anymore.


def getConstantValueGuide(constant, elements_only):
    # Many cases and all return, that is how we do it here,
    # pylint: disable=too-many-return-statements
    constant_type = type(constant)

    if constant_type in (
        str,
        unicode,
        complex,
        int,
        long,
        bool,
        float,
        NoneType,
        range,
        bytes,
        slice,
        xrange,
        type,
        frozenset,
        BuiltinFunctionType,
        EllipsisType,
    ):
        return "i"
    elif constant_type is list:
        for element in constant:
            if isMutable(element):
                return ("%s" if elements_only else "L%s") % (
                    "".join(
                        getConstantValueGuide(element, elements_only=False)
                        for element in constant
                    ),
                )

        return "l"

    elif constant_type is tuple:
        # Empty tuples need no deep copy, not even a copy.
        if not constant:
            return "i"

        return ("%s" if elements_only else "T%s") % (
            "".join(
                getConstantValueGuide(element, elements_only=False)
                for element in constant
            ),
        )
    elif constant_type is set:
        return "S"
    elif constant_type is bytearray:
        return "B"
    elif constant_type is dict:
        for value in iterItems(constant):
            if isMutable(value):
                return "D"

        return "d"
    else:
        return "?"


#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
