# Yelight Smart Bulb plugin
# Specification: http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf
#
# Supported commands: toggle, set_power, set_bright, start_cf, stop_cf, set_scene,
# cron_add, cron_get, cron_del, set_adjust, set_name, get_prop.
#
# Unsupported (feel free to implement something of this): set_ct_abx, set_rgb,
# set_hsv, set_music.

import json
import re
import socket
import time

from web.w import UpdatesHandler
from utils import get_store

STORE_KEY = 'yeelight'
DEVICE = 'yeelight'
YEE_DISCOVER = [
    'M-SEARCH * HTTP/1.1',
    'MAN: "ssdp:discover"',
    'ST: wifi_bulb'
]
store = get_store()


def toggle(device_id=None):
    """
    Toggle the smart LED.
    """
    return send_command({
        'method': 'toggle',
        'params': []
    }, device_id)


def set_power(power, effect='smooth', duration=500, device_id=None):
    """
    Switch on or off the smart LED.
    `power` - Boolean
    `effect` - "smooth" or "sudden"
    `duration` - duration of smooth effect, ignored if effect is "sudden"
    """
    return send_command({
        'method': 'set_power',
        'params': ['on' if power else 'off', effect, duration]
    }, device_id)


def set_bright(brightness, effect='smooth', duration=500, device_id=None):
    """
    Change the brightness of a smart LED.
    `brightness` - integer from 1 to 100
    """
    return send_command({
        'method': 'set_bright',
        'params': [int(brightness), effect, int(duration)]
    }, device_id)


def start_cf(flow_expression, count=1, action=0, device_id=None):
    """
    Start a color flow (series of smart LED visible state changes; see the Yeelight specification for details).
    `flow_expression` - list of tuples contains 4 elements (duration, mode, value, brightness). Example: [(1000, 2, 2700, 100), (500, 1, 255, 10)]. Again, see the specification for details.
    `count` - how many times perform flow expression (0 - loop forever)
    `action` - action taken after the flow is stopped (0 - recover to the state before; 1 - stay where the flow is stopped; 2 - turn off smart LED)
    """
    return send_command({
        'method': 'start_cf',
        'params': [count, action, ', '.join([', '.join(map(str, x)) for x in flow_expression])]
    }, device_id)


def stop_cf(device_id=None):
    """
    Stop a running color flow.
    """
    return send_command({
        'method': 'stop_cf',
        'params': []
    }, device_id)


def set_scene(state_type, *args, device_id=None):
    """
    Set the smart LED directly to specified state. See the specification for details.
    `state_type` - color|hsv|ct|cf|auto_delay_off
    `*args` - state type specific parameters
    """
    return send_command({
        'method': 'set_scene',
        'params': [state_type] + list(args)
    }, device_id)


def cron_add(state_type, value, device_id=None):
    """
    Start a time job on the smart LED.
    `state_type` - currently can only be 0 (means power off)
    `value` - length of the timer (minutes)
    """
    return send_command({
        'method': 'cron_add',
        'params': [int(state_type), value]
    }, device_id)


def cron_get(state_type, device_id=None):
    """
    Retrieve current cron jobs of the specified type.
    """
    return send_command({
        'method': 'cron_get',
        'params': [int(state_type)]
    }, device_id)


def cron_del(state_type, device_id=None):
    """
    Stop the specified cron job.
    """
    return send_command({
        'method': 'cron_del',
        'params': [int(state_type)]
    }, device_id)


def set_adjust(action, prop, device_id=None):
    """
    Adjust brightness, color temperature or color.
    `action` - increase|decrease|circle
    `prop` - property to adjust: bright|ct|color
    """
    return send_command({
        'method': 'set_adjust',
        'params': [action, prop]
    }, device_id)


def set_name(name, device_id=None):
    """
    Set name for a smart LED.
    """
    return send_command({
        'method': 'set_name',
        'params': [name]
    }, device_id)


def get_prop(properties=['name', 'power', 'bright'], device_id=None):
    """
    Retrieve current properties of a smart LED.
    """
    results = send_command({
        'method': 'get_prop',
        'params': properties
    }, device_id)

    # Populate stored values
    for device in results:
        data = dict(zip(properties, device['result']))
        update_device(data, device['id'])

    return results


def process(data):
    data = get_data(data)
    if 'id' not in data:
        return
    add_device(data)


def discover():
    from mihome import MULTICAST, SOCKET_BUFSIZE

    # Empty stored list
    store.delete(STORE_KEY)

    address, port = MULTICAST.get('yeelight')
    yee_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    yee_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32)
    yee_socket.sendto('\r\n'.join(YEE_DISCOVER).encode(), (address, port))
    
    while True:
        data, _ = yee_socket.recvfrom(SOCKET_BUFSIZE)
        add_device(get_data(data.decode()))


def get_devices():
    stored = store.get(STORE_KEY)
    return json.loads(stored.decode() if stored else '[]')


def add_device(data):
    devices = get_devices()
    for i, device in enumerate(devices[:]):
        if device['id'] == data['id']:
            devices.pop(i)
    devices.append(data)
    store.set(STORE_KEY, json.dumps(devices))


def update_device(data, device_id):
    devices = get_devices()
    for i, device in enumerate(devices[:]):
        if device['id'] == device_id:
            devices[i].update(data)
    store.set(STORE_KEY, json.dumps(devices))


def get_data(data):
    data = dict(re.findall(r'(\w+):(.*)', data.replace('\r', '')))
    for key in data:
        data[key] = data[key].strip()
    return data


def send_command(command, device_id=None):
    """
    Send command to a bulb. If no bulb id specified, send command to all smart LED bulbs
    """
    devices = get_devices()
    if device_id is not None:
        devices = filter(lambda x: x['id'] == device_id, devices)
    results = []
    for device in devices:
        command.update(id=device['id'])
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(3)
        address, port = device['Location'].replace('yeelight://', '').split(':')
        s.connect((address, int(port)))
        s.send(json.dumps(command).encode('ascii') + b'\r\n')
        result = json.loads(s.recv(1024).decode())
        s.close()
        result.update(id=device['id'])
        results.append(result)

        # Send updates to web
        if command['method'] != 'get_prop':
            props = get_prop(device_id=device['id'])
            if props:
                name, power, bright = props[0]['result']
                data = {
                    'device': DEVICE,
                    'id': device['id'],
                    'name': name,
                    'power': power,
                    'bright': bright
                }
                UpdatesHandler.send_updates(data)

    return results