# coding: utf-8

"""
Pi-Dom is module to make domotic with RPi and.
With use emit from `<http://www.noopy.fr/raspberry-pi/domotique/>`_
``emit`` can communicate with Chacon 54795
"""

import time
import subprocess
import pickle

from pathlib import Path

__all__ = ['PiDom', 'event']
__version__ = "0.3.2"
__author__ = "Oprax"
__license__ = "MIT"


def event(name):
    """
    Pattern Observer with decorator,
    can execute function when event fire

    :param name: name of event
    :type name: str
    :rtype: function
    """
    event.sub = getattr(event, 'sub', dict())

    event.trigger = lambda e, data: [f(e, data) for f in event.sub[e]]

    def decorator(func):
        event.sub.setdefault(name, []).append(func)
        return func

    return decorator


@event('pidom.update')
def update(*args, **kwargs):
    """
    This function register 'pidom.update' event.
    This event is send each time a switch is turn on/off.
    """
    pass


@event('pidom.delete')
def delete(*args, **kwargs):
    """
    This function register 'pidom.delete' event.
    This event is send each time a switch is unsynchronize.
    """
    pass


class PiDom(object):
    """This class is a wrapper around `emit`"""
    def __init__(self):
        """Create property and call `PiDom.restore`"""
        self._bak = Path('~/.pidom.bin').expanduser()
        self._register = dict()
        self._groups = dict()
        self._id_available = set(range(0x00A0A400, 0x00A0A400 + 20))
        self.restore()

    def _change_state_device(self, name, state=True):
        """
        Change the state of a switch by using the device name

        :param name: Name of device
        :param state: The state we want, True for On and False for Off
        :type name: str
        :type state: bool
        """
        device_id = self._register[name]['device_id']
        self._register[name]['state'] = state
        # http://stackoverflow.com/a/14678150
        args = ['sudo', 'emit', '-d', format(device_id, '02X'), '-b', 'A1']
        if not state:
            args.append('-x')
        try:
            subprocess.run(args, check=True, stdout=subprocess.PIPE)
        except subprocess.CalledProcessError as e:
            print("https://github.com/Oprax/pidom#install")
            raise e
        event.trigger('pidom.update', {'name': name, 'state': state})

    def _sanitize(self, names):
        """
        Parse argument to return a list,
        args could be a device name, a group or just a list of devices

        :param names: a device name, a group or list of devices
        :param type: str | iterable
        :return: return a list of device(s)
        :rtype: iterable
        """
        if isinstance(names, str):
            if names in self._groups.keys():
                names = self._groups[names]
            else:
                names = [names]
        return [name.strip() for name in names]

    def backup(self):
        """
        Save register and groups in '~/.pidom.bin'
        Using ``pickle`` module to serialize data
        """
        if not self._bak.exists() and not self._bak.is_file():
            self._bak.touch()
        data = {
            'register': self._register,
            'groups': self._groups
        }
        pickle.dump(data, self._bak.open(mode='wb'))

    def restore(self):
        """
        Restore information (register and groups) from '~/.pidom.bin'
        Using ``pickle`` module to unserialize data
        Reconstruct id_available from register
        """
        if not self._bak.exists() and not self._bak.is_file():
            return
        data = pickle.load(self._bak.open(mode='rb'))
        self._groups = data['groups']
        self._register = data['register']
        for name in self._register.keys():
            self._id_available.remove(
                self._register[name]['device_id'])

    def switch_on(self, names):
        """
        Turn on a device or a group of devices

        :param names: Name(s) of device(s)
        :type names: str | iterable
        :return: all devices updated
        :rtype: list
        """
        names = self._sanitize(names)
        res = list()
        for name in names:
            self._change_state_device(name, state=True)
            res.append(dict(name=name, state=True))
        return res

    def switch_off(self, names):
        """
        Turn off a device or a group of devices

        :param names: Name(s) of device(s)
        :type names: str | iterable
        :return: all devices updated
        :rtype: list
        """
        names = self._sanitize(names)
        res = list()
        for name in names:
            self._change_state_device(name, state=False)
            res.append(dict(name=name, state=False))
        return res

    def toggle(self, names):
        """
        Reverse state of a device

        :param names: Name(s) of device
        :return: all devices updated
        :rtype: list(s)
        :type names: str | iterable
        """
        names = self._sanitize(names)
        res = list()
        for name in names:
            new_state = not self._register[name]['state']
            self._change_state_device(name, state=new_state)
            res.append(dict(name=name, state=new_state))
        return res

    def synchronize(self, name):
        """
        Synchronize a new device and add it to register.

        When a Chacon is plugged in,
        while 5 seconds he listen and wait his id.

        :param name: device name given by user, most easy than 0x00A0A408
        :type name: str
        :return: Id of the device
        :rtype: int
        """
        name = name.strip()
        if name in self._register.keys():
            raise ValueError("name '{}' already used !".format(name))

        device_id = self._id_available.pop()
        self._register[name] = {
            'device_id': device_id,
            'state': False
        }
        t1 = time.time()
        t2 = time.time()
        while (t2 - t1) < 5:
            self.switch_on(name)
            t2 = time.time()
        self.switch_off(name)
        return device_id

    def unsynchronize(self, name, del_entry=True):
        """Remove the device from register and place id on list of available"""
        name = name.strip()
        self.switch_off(name)
        self._id_available.add(
            self._register[name]['device_id'])
        if del_entry:
            del self._register[name]
        event.trigger('pidom.delete', {'name': name, 'state': False})

    def clear(self):
        """Unsynchronize all device in register"""
        for name in self._register.keys():
            self.unsynchronize(name, del_entry=False)
        self._register.clear()

    def reset(self):
        """Reset all device in register"""
        for name in self._register.keys():
            self.switch_off(name)

    def state(self, name):
        """
        Return the state of device by name

        :param name: device name
        :type name: str
        :return: State of device
        :rtype: bool
        """
        return self._register[name]['state']

    @property
    def devices(self):
        """
        Return a list of device

        :rtype: list
        """
        return list(self._register.keys())

    @property
    def groups(self):
        """
        Return a list of group

        :rtype: dict
        """
        return dict(self._groups)

    def new_group(self, group_name, devices):
        """
        A group is a list of device

        :param group_name: group name given by user
        :param devices: list of device in a group
        :type group_name: str
        :type devices: iterable
        """
        self._groups[group_name] = set()
        for device in devices:
            if device in self._register.keys():
                self.switch_off(device)
                self._groups[group_name].add(device)

    def rm_group(self, group_name):
        """
        Remove the list

        :param group_name: group name given by user
        :type group_name: str
        """
        self.switch_off(group_name)
        del self._groups[group_name]
