from contextlib import suppress
from dataclasses import dataclass
from functools import partial

from pygments import highlight
from pygments.formatters import TerminalTrueColorFormatter
from pygments.lexers import ObjectiveCLexer

from rpcclient.darwin import objc
from rpcclient.darwin.objective_c_class import Class
from rpcclient.darwin.symbol import DarwinSymbol
from rpcclient.exceptions import RpcClientException
from rpcclient.symbol import Symbol
from rpcclient.symbols_jar import SymbolsJar


class SettingIvarError(RpcClientException):
    """ Raise when trying to set an Ivar too early or when the Ivar doesn't exist. """
    pass


@dataclass
class Ivar:
    name: str
    value: DarwinSymbol
    type_: str
    offset: int


class ObjectiveCSymbol(DarwinSymbol):
    """
    Wrapper object for an objective-c symbol.
    Allowing easier access to its properties, methods and ivars.
    """

    @classmethod
    def create(cls, value: int, client):
        """
        Create an ObjectiveCSymbol object.
        :param value: Symbol address.
        :param rpcclient.darwin.client.Client client: client.
        :return: ObjectiveCSymbol object.
        :rtype: ObjectiveCSymbol
        """
        symbol = super().create(value, client)
        symbol.ivars = []
        symbol.properties = []
        symbol.methods = []
        symbol.class_ = None  # type: Class
        symbol.reload()
        return symbol

    def reload(self):
        """
        Reload object's in-memory layout.
        """
        object_data = self._client.showobject(self)

        ivars_list = [
            Ivar(name=ivar['name'], type_=ivar['type'], offset=ivar['offset'],
                 value=ivar['value']) for ivar in object_data['ivars']
        ]
        methods_list = [
            objc.Method.from_data(method, self._client) for method in object_data['methods']
        ]
        properties_list = [
            objc.Property(name=prop['name'], attributes=objc.convert_encoded_property_attributes(prop['attributes']))
            for prop in object_data['properties']
        ]

        class_object = Symbol.create(object_data['class_address'], self._client)
        class_wrapper = Class(self._client, class_object)

        self.ivars = ivars_list
        self.methods = methods_list
        self.properties = properties_list
        self.class_ = class_wrapper

    def show(self, recursive: bool = False):
        """
        Print to terminal the highlighted class description.
        :param recursive: Show methods of super classes.
        """
        print(highlight(self._to_str(recursive), ObjectiveCLexer(), TerminalTrueColorFormatter(style='native')))

    def objc_call(self, selector: str, *params, **kwargs):
        """
        Make objc_call() from self return ObjectiveCSymbol when it's an objc symbol.
        :param selector: Selector to execute.
        :param params: Additional parameters.
        :return: ObjectiveCSymbol when return type is an objc symbol.
        """
        symbol = super(ObjectiveCSymbol, self).objc_call(selector, *params, **kwargs)
        return symbol.objc_symbol if self._client.is_objc_type(symbol) else symbol

    def _set_ivar(self, name, value):
        try:
            ivars = self.__getattribute__('ivars')
            class_name = self.__getattribute__('class_').name
        except AttributeError as e:
            raise SettingIvarError from e

        for i, ivar in enumerate(ivars):
            if ivar.name == name:
                size = self.item_size
                if i < len(self.ivars) - 1:
                    size = ivars[i + 1].offset - ivar.offset
                with self.change_item_size(size):
                    self[ivar.offset // size] = value
                    ivar.value = value
                return
        raise SettingIvarError(f'Ivar "{name}" does not exist in "{class_name}"')

    def _to_str(self, recursive=False):
        protocols_buf = f'<{",".join(self.class_.protocols)}>' if self.class_.protocols else ''

        if self.class_.super is not None:
            buf = f'@interface {self.class_.name}: {self.class_.super.name} {protocols_buf}\n'
        else:
            buf = f'@interface {self.class_.name} {protocols_buf}\n'

        # Add ivars
        buf += '{\n'
        for ivar in self.ivars:
            buf += f'\t{ivar.type_} {ivar.name} = 0x{int(ivar.value):x}; // 0x{ivar.offset:x}\n'
        buf += '}\n'

        # Add properties
        for prop in self.properties:
            attrs = prop.attributes
            buf += f'@property ({",".join(attrs.list)}) {prop.attributes.type_} {prop.name};\n'

            if attrs.synthesize is not None:
                buf += f'@synthesize {prop.name} = {attrs.synthesize};\n'

        # Add methods
        methods = self.methods.copy()

        # Add super methods.
        if recursive:
            for sup in self.class_.iter_supers():
                for method in filter(lambda m: m not in methods, sup.methods):
                    methods.append(method)

        # Print class methods first.
        methods.sort(key=lambda m: not m.is_class)

        for method in methods:
            buf += str(method)

        buf += '@end'
        return buf

    @property
    def symbols_jar(self) -> SymbolsJar:
        """ Get a SymbolsJar object for quick operations on all methods """
        jar = SymbolsJar.create(self._client)

        for m in self.methods:
            jar[m.name] = m.address

        return jar

    def __dir__(self):
        result = set()

        for ivar in self.ivars:
            result.add(ivar.name)

        for method in self.methods:
            result.add(method.name.replace(':', '_'))

        for sup in self.class_.iter_supers():
            for method in sup.methods:
                result.add(method.name.replace(':', '_'))

        result.update(list(super(ObjectiveCSymbol, self).__dir__()))
        return list(result)

    def __getitem__(self, item):
        if isinstance(item, int):
            return super(ObjectiveCSymbol, self).__getitem__(item)

        # Ivars
        for ivar in self.ivars:
            if ivar.name == item:
                if self._client.is_objc_type(ivar.value):
                    return ivar.value.objc_symbol
                return ivar.value

        # Properties
        for prop in self.properties:
            if prop.name == item:
                return self.objc_call(item)

        # Methods
        for method in self.methods:
            if method.name == item:
                return partial(self.class_.objc_call, item) if method.is_class else partial(self.objc_call, item)

        for sup in self.class_.iter_supers():
            for method in sup.methods:
                if method.name == item:
                    return partial(self.class_.objc_call, item) if method.is_class else partial(self.objc_call, item)

        raise AttributeError(f''''{self.class_.name}' has no attribute {item}''')

    def __getattr__(self, item: str):
        return self[self.class_.sanitize_name(item)]

    def __setitem__(self, key, value):
        if isinstance(key, int):
            super(ObjectiveCSymbol, self).__setitem__(key, value)
            return

        with suppress(SettingIvarError):
            self._set_ivar(key, value)
            return

    def __setattr__(self, key, value):
        try:
            key = self.__getattribute__('class_').sanitize_name(key)
        except AttributeError:
            pass
        try:
            self._set_ivar(key, value)
        except SettingIvarError:
            super(ObjectiveCSymbol, self).__setattr__(key, value)

    def __str__(self):
        return self._to_str(False)
