# Released under the MIT License. See LICENSE for details.
#
"""Misc utility functionality related to the entity system."""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Any, Union, Tuple, List
    from efro.entity._value import CompoundValue
    from efro.entity._support import BoundCompoundValue


def diff_compound_values(
        obj1: Union[BoundCompoundValue, CompoundValue],
        obj2: Union[BoundCompoundValue, CompoundValue]) -> str:
    """Generate a string showing differences between two compound values.

    Both must be associated with data and have the same set of fields.
    """

    # Ensure fields match and both are attached to data...
    value1, data1 = get_compound_value_and_data(obj1)
    if data1 is None:
        raise ValueError(f'Invalid unbound compound value: {obj1}')
    value2, data2 = get_compound_value_and_data(obj2)
    if data2 is None:
        raise ValueError(f'Invalid unbound compound value: {obj2}')
    if not have_matching_fields(value1, value2):
        raise ValueError(
            f"Can't diff objs with non-matching fields: {value1} and {value2}")

    # Ok; let 'er rip...
    diff = _diff(obj1, obj2, 2)
    return '  <no differences>' if diff == '' else diff


class CompoundValueDiff:
    """Wraps diff_compound_values() in an object for efficiency.

    It is preferable to pass this to logging calls instead of the
    final diff string since the diff will never be generated if
    the associated logging level is not being emitted.
    """

    def __init__(self, obj1: Union[BoundCompoundValue, CompoundValue],
                 obj2: Union[BoundCompoundValue, CompoundValue]):
        self._obj1 = obj1
        self._obj2 = obj2

    def __repr__(self) -> str:
        return diff_compound_values(self._obj1, self._obj2)


def _diff(obj1: Union[BoundCompoundValue, CompoundValue],
          obj2: Union[BoundCompoundValue, CompoundValue], indent: int) -> str:
    from efro.entity._support import BoundCompoundValue
    bits: List[str] = []
    indentstr = ' ' * indent
    vobj1, _data1 = get_compound_value_and_data(obj1)
    fields = sorted(vobj1.get_fields().keys())
    for field in fields:
        val1 = getattr(obj1, field)
        val2 = getattr(obj2, field)
        # for nested compounds, dive in and do nice piecewise compares
        if isinstance(val1, BoundCompoundValue):
            assert isinstance(val2, BoundCompoundValue)
            diff = _diff(val1, val2, indent + 2)
            if diff != '':
                bits.append(f'{indentstr}{field}:')
                bits.append(diff)
        # for all else just do a single line
        # (perhaps we could improve on this for other complex types)
        else:
            if val1 != val2:
                bits.append(f'{indentstr}{field}: {val1} -> {val2}')
    return '\n'.join(bits)


def have_matching_fields(val1: CompoundValue, val2: CompoundValue) -> bool:
    """Return whether two compound-values have matching sets of fields.

    Note this just refers to the field configuration; not data.
    """
    # Quick-out: matching types will always have identical fields.
    if type(val1) is type(val2):
        return True

    # Otherwise do a full comparison.
    return val1.get_fields() == val2.get_fields()


def get_compound_value_and_data(
    obj: Union[BoundCompoundValue,
               CompoundValue]) -> Tuple[CompoundValue, Any]:
    """Return value and data for bound or unbound compound values."""
    # pylint: disable=cyclic-import
    from efro.entity._support import BoundCompoundValue
    from efro.entity._value import CompoundValue
    if isinstance(obj, BoundCompoundValue):
        value = obj.d_value
        data = obj.d_data
    elif isinstance(obj, CompoundValue):
        value = obj
        data = getattr(obj, 'd_data', None)  # may not exist
    else:
        raise TypeError(
            f'Expected a BoundCompoundValue or CompoundValue; got {type(obj)}')
    return value, data


def compound_eq(obj1: Union[BoundCompoundValue, CompoundValue],
                obj2: Union[BoundCompoundValue, CompoundValue]) -> Any:
    """Compare two compound value/bound-value objects for equality."""

    # Criteria for comparison: both need to be a compound value
    # and both must have data (which implies they are either a entity
    # or bound to a subfield in a entity).
    value1, data1 = get_compound_value_and_data(obj1)
    if data1 is None:
        return NotImplemented
    value2, data2 = get_compound_value_and_data(obj2)
    if data2 is None:
        return NotImplemented

    # Ok we can compare them. To consider them equal we look for
    # matching sets of fields and matching data.  Note that there
    # could be unbound data causing inequality despite their field
    # values all matching; not sure if that's what we want.
    return have_matching_fields(value1, value2) and data1 == data2
