"""Data model for GE kitchen appliances"""

import enum
import logging
from weakref import WeakValueDictionary
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, Union
from slixmpp import JID

from .erd import ErdCode, ErdCodeType, ErdCodeClass, ErdApplianceType, ErdEncoder
from .exception import *

if TYPE_CHECKING:
    from .clients import GeBaseClient

try:
    import ujson as json
except ImportError:
    import json

_LOGGER = logging.getLogger(__name__)


class GeAppliance:
    """Base class shared by all appliances"""

    # Registry of initialized appliances
    _appliance_cache = WeakValueDictionary()

    def __new__(cls, mac_addr: Union[str, JID], client: "GeBaseClient", *args, **kwargs):
        if isinstance(mac_addr, JID):
            mac_addr = str(mac_addr.user).split('_')[0]
        try:
            obj = cls._appliance_cache[mac_addr]  # type: "GeAppliance"
        except KeyError:
            obj = super(GeAppliance, cls).__new__(cls)
            obj.__init__(mac_addr, client)
            cls._appliance_cache[obj.mac_addr] = obj
            return obj
        else:
            if client.client_priority >= obj.client.client_priority:
                obj.client = client
        return obj

    def __init__(self, mac_addr: Union[str, JID], client: "GeBaseClient"):
        if isinstance(mac_addr, JID):
            mac_addr = str(mac_addr.user).split('_')[0]
        self._available = False
        self._mac_addr = mac_addr.upper()
        self._message_id = 0
        self._property_cache = {}  # type: Dict[ErdCodeType, Any]
        self.client = client
        self.initialized = False
        self._encoder = ErdEncoder()

    @property
    def mac_addr(self) -> str:
        return self._mac_addr.upper()

    @property
    def known_properties(self) -> Set[ErdCodeType]:
        return set(self._property_cache)

    async def async_request_update(self):
        """Request the appliance send a full state update"""
        await self.client.async_request_update(self)

    def set_available(self):
        _LOGGER.debug(f'{self.mac_addr} marked available')
        self._available = True

    def set_unavailable(self):
        _LOGGER.debug(f'{self.mac_addr} marked unavailable')
        self._available = False

    @property
    def available(self) -> bool:
        return self._available and self.initialized

    @property
    def appliance_type(self) -> Optional[ErdApplianceType]:
        return self._property_cache.get(ErdCode.APPLIANCE_TYPE)

    def translate_erd_code(self, erd_code: ErdCodeType) -> ErdCodeType:
        """
        Translates a code to it's native value or a string if it is not known.
        """
        return self._encoder.translate_code(erd_code)

    def decode_erd_value(self, erd_code: ErdCodeType, erd_value: str) -> Any:
        """
        Decode and ERD Code raw value into something useful.  If the erd_code is a string that
        cannot be resolved to a known ERD Code, the value will be treated as raw byte string.
        Unregistered ERD Codes will be translated as ints.

        :param erd_code: ErdCode or str, the ERD Code the value of which we want to decode
        :param erd_value: The raw ERD code value, usually a hex string without leading "0x"
        :return: The decoded value.
        """
        return self._encoder.decode_value(erd_code, erd_value)

    def encode_erd_value(self, erd_code: ErdCodeType, value: Any) -> str:
        """
        Encode an ERD Code value as a hex string.
        Only ERD Codes registered with self.erd_encoders will processed.  Otherwise an error will be returned.

        :param erd_code: ErdCode or str, the ERD Code the value of which we want to decode
        :param value: The value to re-encode
        :return: The encoded value as a hex string
        """
        return self._encoder.encode_value(erd_code, value)

    def get_erd_value(self, erd_code: ErdCodeType) -> Any:
        """
        Get the value of a property represented by an ERD Code
        :param erd_code: ErdCode or str, The ERD code for the property to get
        :return: The current cached value of that ERD code
        """
        erd_code = self._encoder.translate_code(erd_code)
        return self._property_cache[erd_code]

    def get_erd_code_class(self, erd_code: ErdCodeType) -> ErdCodeClass:
        """
        Get the classification for a given ErdCode
        """
        return self._encoder.get_code_class(erd_code)

    async def async_set_erd_value(self, erd_code: ErdCodeType, value: Any):
        """
        Send a new erd value to the appliance.
        :param erd_code: The ERD code to update
        :param value: The new value to set
        """
        erd_value = self.encode_erd_value(erd_code, value)
        await self.client.async_set_erd_value(self, erd_code, erd_value)

    def update_erd_value(
            self, erd_code: ErdCodeType, erd_value: str) -> bool:
        """
        Setter for ERD code values.

        :param erd_code: ERD code to update
        :param erd_value: The new value to set, as returned by the appliance (usually a hex string)
        :return: Boolean, True if the state changed, False if no value changed
        """
        erd_code = self._encoder.translate_code(erd_code)
        value = self.decode_erd_value(erd_code, erd_value)

        old_value = self._property_cache.get(erd_code)

        try:
            state_changed = ((old_value is None) != (value is None)) or (old_value != value)
        except ValueError:
            _LOGGER.info('Unable to compare new and prior states.')
            state_changed = False

        if state_changed:
            _LOGGER.debug(f'Setting {erd_code} to {value}')
        self._property_cache[erd_code] = value

        return state_changed

    def update_erd_values(self, erd_values: Dict[ErdCodeType, str]) -> Dict[ErdCodeType, Any]:
        """
        Set one or more ERD codes value at once

        :param erd_values: Dictionary of erd codes and their new values as raw hex strings
        :return: dictionary of new states
        """
        state_changes = {
            self._encoder.translate_code(k): self.decode_erd_value(k, v)
            for k, v in erd_values.items()
            if self.update_erd_value(k, v)
        }

        return state_changes

    def stringify_erd_value(self, value: Any, **kwargs) -> Optional[str]:
        """
        Stringifies a code value if possible.  If it can't be stringified, returns none.
        By default, enums are stringified using their title-cased name (after replacing 
        underscores)
        """

        try:
            if not value:
                return None

            stringify_op = getattr(value, "stringify", None)
            if callable(stringify_op):
                return value.stringify(**kwargs)
            elif isinstance(value, enum.Enum):
                return value.name.replace("_"," ").title()
            else:
                return str(value)
        except (ValueError, KeyError):
            return None

    def boolify_erd_value(self, value: Any) -> Optional[bool]:
        """
        Boolifies a code value if possible.  If it can't be boolified, returns none
        """

        try:
            if not value:
                return None
            if isinstance(value, bool):
                return value

            boolify_op = getattr(value, "boolify", None)
            if callable(boolify_op):
                return value.boolify()
            else:
                return None
        except:
            return None

    def __str__(self):
        appliance_type = self.appliance_type
        if appliance_type is None:
            appliance_type = 'Unknown Type'
        return f'{self.__class__.__name__}({self.mac_addr}) ({appliance_type})'

    def __format__(self, format_spec):
        return str(self).__format__(format_spec)
