import logging
import json
import collections
from contextlib import suppress, contextmanager

from . import NetworkError


log = logging.getLogger(__name__)


def update(d, u):
    for k, v in u.items():
        if k in d and isinstance(v, collections.abc.Mapping):
            d[k] = update(d[k], v)
        elif v is not None:
            d[k] = v
    return d


class RenderObj(object):
    def __init__(self, name=None, ctrl=None):
        self.name = name
        self.ctrl = ctrl

    def get_state(self):
        return self.ctrl.get_state()[self.name]

    def get(self, *args, **kwargs):
        return self.get_state().get(*args, **kwargs)

    def fetch(self, **kwargs):
        return self.ctrl.fetch(self.name, **kwargs)

    def remove(self):
        return self.ctrl.remove(self.name)

    def update(self, **kwargs):
        return self.ctrl.update(self.name, **kwargs)


class RenderControl(RenderObj):
    def __init__(self, socket_factory):
        # store a list of remote objects created by this to remove on close
        self._refs = []
        self.socket_factory = socket_factory
        self.skt = socket_factory.get_render_socket()

    def _transfer(self, data):
        json_data = json.dumps(data).encode('utf8')
        rep = json.loads(self.skt.transfer(json_data))

        if 'error' in rep:
            raise NetworkError(rep['error'])
        return rep

    def get_state(self):
        resp = self._transfer({})
        return resp

    def attach(self):
        pass

    def create(self, name, type_, **kwargs):
        """ Create a remote object

        kwargs are forwarded as extra params for target object
        """
        msg = {}
        msg[name] = dict(CREATE=type_)
        msg[name].update(kwargs)
        self._transfer(msg)
        self._refs.append(name)
        return RenderObj(name, self)

    def get(self, name):
        if name in self.get_state():
            return RenderObj(name, self)
        return None

    def fetch(self, name, **kwargs):
        json_data = json.dumps(dict(FETCH={name: kwargs})).encode('utf8')
        return self.skt.transfer(json_data)

    def remove(self, name):
        """ Remove a remote object
        """
        self._transfer({name: dict(REMOVE=True)})

    def update(self, name, **kwargs):
        """ forward kwargs to the given remote object
        """
        return self._transfer({name: kwargs})

    def teardown(self):
        """ Teardown the entire render server
        """
        self._transfer(dict(TEARDOWN=True))

    def close(self):
        for ref in self._refs:
            with suppress(Exception):
                self.remove(ref)
        self.skt.close()


class CacheableRenderControl(RenderControl):
    def __init__(self, socket_factory):
        super().__init__(socket_factory)
        self._cached = False

    def _transfer(self, data):
        if self._cached:
            update(self._cached_state, data)
            update(self._cached_write, data)
            return self._cached_state
        return super()._transfer(data)

    def get_state(self):
        if self._cached:
            return self._cached_state
        return super().get_state()

    @contextmanager
    def cached(self):
        c = self._cached  # local copy to maintain reentrant scope
        if not c:
            self._cached_state = self.get_state()
            self._cached_write = {}
            self._cached = True

        try:
            yield self
        finally:
            # don't write if an error occured
            if not c:
                self._cached = False

        if not c:
            self._transfer(self._cached_write)
