"""
Contains the support class for managing attributes whose data is any type of data, determined at runtime
"""
from typing import List

from ..keys import CudaPointerValues, MemoryTypeValues
from ..utils import _EXTENDED_TYPE_ANY, IndentedOutput, to_usd_docs, value_as_usd
from .AttributeManager import AttributeManager, CppConfiguration, CudaConfiguration


class AnyAttributeManager(AttributeManager):
    """
    Support class for attributes whose type is only determined at runtime.

    Most of the generated code is removed for this type of attribute since the interface types are not yet known.
    """

    OGN_TYPE = "any"
    CPP_CONFIGURATION = {
        # Type information is overridden but the include file is important to specialize
        "any": CppConfiguration(None, include_files=["omni/graph/core/ogn/UsdTypes.h"])
    }
    CUDA_CONFIGURATION = {"any": CudaConfiguration(None, cast_required=False)}

    # ----------------------------------------------------------------------
    def requires_default(self):
        """Extended types never need default values as their data types are not known in advance"""
        return False

    # ----------------------------------------------------------------------
    def validate_value(self, value):
        """All values are welcome"""
        return True

    # ----------------------------------------------------------------------
    def validate_value_structure(self, value_to_validate):
        """we can't validate until runtime"""
        return True

    # ----------------------------------------------------------------------
    @staticmethod
    def array_depths_supported() -> List[int]:
        """The meaning of an array of mixed union types is unclear and will not be supported at this time"""
        return [0]

    # ----------------------------------------------------------------------
    @staticmethod
    def tuples_supported() -> List[int]:
        """USD supports only these tuples natively so restrict support to them for now"""
        return [1]

    # ----------------------------------------------------------------------
    def cpp_configuration(self) -> CppConfiguration:
        """Returns the C++ configuration data that applies to the attribute type implemented by this manager
        If no implementation is defined then return an empty dictionary.
        """
        try:
            return self.CPP_CONFIGURATION["any"]
        except AttributeError:
            return CppConfiguration("any")

    # ----------------------------------------------------------------------
    def cpp_extended_type(self):
        """Returns the extended type identifier for C++ types"""
        return "kExtendedAttributeType_Any"

    # ----------------------------------------------------------------------
    def cpp_base_type_name(self):
        """Returns a string with the C++ type of the attribute data
        This value relies on the fact that the group names correspond to the template parameters for RuntimeAttribute.
        """
        template_args = [self.attribute_group, MemoryTypeValues.CPP[self.memory_storage()]]
        if self.cuda_pointer_type is not None:
            template_args.append(CudaPointerValues.CPP[self.cuda_pointer_type])
        return f"ogn::RuntimeAttribute<{', '.join(template_args)}>"

    # ----------------------------------------------------------------------
    def cpp_element_type_name(self) -> str:
        """The configuration is all manual here so override the default method"""
        return self.cpp_base_type_name()

    # ----------------------------------------------------------------------
    def cpp_includes(self) -> List[str]:
        """Tack on the include implementing the runtime attribute wrappers"""
        regular_includes = super().cpp_includes()
        regular_includes.append("omni/graph/core/ogn/RuntimeAttribute.h")
        return regular_includes

    # ----------------------------------------------------------------------
    def cpp_accessor_on_cpu(self) -> bool:
        """Extended type wrappers provide a type-casting accessor that will always live on the CPU"""
        return True

    # ----------------------------------------------------------------------
    def fabric_data_variable_name(self) -> str:
        """This type uses a local variable to encapsulate the Fabric data so reference that instead."""
        return f"{self.cpp_variable_name()}_"

    # ----------------------------------------------------------------------
    def datamodel_accessor_constructor_args(self) -> List[str]:
        """The runtime attribute is a local variable so the pointer has to be added to the constructor"""
        return [f"&{self.fabric_data_variable_name()}"] + super().datamodel_accessor_constructor_args()

    # ----------------------------------------------------------------------
    def datamodel_local_variables(self):
        """The runtime attributes require a wrapper for data access (RuntimeAttribute). Create one here to avoid
        the overhead of recreating it every time access to it is needed, and to avoid needing a parallel set of
        accessor classes just for extended attribute types.
        """
        return [f"{self.fabric_raw_type()} {self.fabric_data_variable_name()}{{}};"]

    # ----------------------------------------------------------------------
    def has_fixed_type(self) -> bool:
        """Variable typed attributes have runtime type identification"""
        return False

    # ----------------------------------------------------------------------
    def ogn_type(self) -> List:
        """Returns a string containing the fully expanded name of this attribute type in a .ogn file"""
        return "any"

    # ----------------------------------------------------------------------
    def python_extended_type(self):
        """Returns the extended type identifier for Python attribute types"""
        return (_EXTENDED_TYPE_ANY, "any")

    # ----------------------------------------------------------------------
    def python_imports(self) -> List[str]:
        """Return a list of modules to import in the Python header for proper parsing of this type"""
        return super().python_imports() + ["from typing import Any"]

    # ----------------------------------------------------------------------
    def python_type_name(self):
        """Returns a string with the Python "typing" type-checking declaration for the attribute data"""
        return self.python_add_containers("Any")

    # ----------------------------------------------------------------------
    def generate_python_property_code(self, out: IndentedOutput):
        """Emits the generated code implementing a readable property for this extended attribute.
        This class overrides the default behaviour because it needs a wrapper class to access the internal
        functionality of the runtime data.
        """
        property_name = self.python_property_name()
        out.write()
        out.write("@property")
        if out.indent(f"def {property_name}(self) -> og.RuntimeAttribute:"):
            out.write(f'"""Get the runtime wrapper class for the attribute {self.namespace}.{property_name}"""')
            out.write(
                f"return og.RuntimeAttribute(self._attributes.{property_name}.get_attribute_data(),"
                f" self._context, {self.is_read_only()})"
            )
            out.exdent()
        # For this type of attribute a setter can forward the assignment to the value of the attribute, where legal
        out.write()
        out.write(f"@{property_name}.setter")
        if out.indent(f"def {property_name}(self, value_to_set: Any):"):
            out.write(f'"""Assign another attribute\'s value to outputs.{property_name}"""')
            if out.indent("if isinstance(value_to_set, og.RuntimeAttribute):"):
                out.write(f"self.{property_name}.value = value_to_set.value")
                out.exdent()
            if out.indent("else:"):
                out.write(f"self.{property_name}.value = value_to_set")
                out.exdent()
            out.exdent()

    # ----------------------------------------------------------------------
    def generate_python_validation(self, out: IndentedOutput):
        """Emit code that checks to make sure the attribute type is resolved before computing"""
        if self.do_validation:
            name = f"{self.namespace}.{self.python_property_name()}"
            if out.indent(f"if db.{name}.type.base_type == og.BaseDataType.UNKNOWN:"):
                out.write(f"db.log_warning('Required extended attribute {self.name} is not resolved, compute skipped')")
                out.write("return False")
                out.exdent()

    # ----------------------------------------------------------------------
    def usd_type_name(self):
        """As the type of the attribute is not known at load time use a token value to describe the accepted types"""
        return "token"

    # ----------------------------------------------------------------------
    def usd_type_accepted_description(self) -> str:
        """Returns a string that will be the default value of the USD token, describing accepted types"""
        return "any"

    # ----------------------------------------------------------------------
    def emit_usd_declaration(self, out):
        """USD declaration for extended types use a placeholder type of token so the code path must be replaced

        Args:
            out: Output handler where the USD will be emitted
        """
        usd_name = self.usd_name()
        usd_type = self.usd_type_name()

        docs = to_usd_docs(self.description)
        if self.array_depth == 0:
            default = value_as_usd(self.usd_type_accepted_description())
        else:
            default = value_as_usd([self.usd_type_accepted_description()] * self.array_depth)

        if out.indent(f"custom {usd_type} {usd_name} = {default} ("):
            out.write(docs)
            out.exdent(")")
