#!/usr/bin/env python

from __future__ import unicode_literals
from simplejson.decoder import JSONDecodeError
import api_response
import config
import device
import group
import light
import logging
import re
import requests
import scene
import schedule
import sensor


logger = logging.getLogger(__name__)


endpoint_regex = re.compile(r'/api/([^/]+)/([^/]+)/([^/]+)(?:/(.+))?')


class HueError(Exception):
    pass


class Bridge(object):
    def __init__(self, hostname, username=None, port=80):
        self.hostname = hostname
        self.port = port
        self._username = username
        self.__contruct_api_url()

    def from_address(self, address):
        m = re.match(endpoint_regex, address)
        assert m, 'Invalid address'
        endpoint = m.group(2)
        hue_id = m.group(3)
        if endpoint == 'sensors':
            return self.sensor(hue_id=hue_id)
        elif endpoint == 'lamps':
            return self.lamp(hue_id=hue_id)
        elif endpoint == 'groups':
            return self.group(hue_id=hue_id)
        elif endpoint == 'schedules':
            return self.schedule(hue_id=hue_id)
        raise HueError('Unsupported enpoint')

    @property
    def username(self):
        return self._username

    @username.setter
    def username(self, value):
        self._username = value
        self.__contruct_api_url()

    def __contruct_api_url(self):
        self.API = 'http://{}:{}/api'.format(
            self.hostname,
            self.port
        )
        if self.username:
            self.API += '/{}'.format(self.username)
        logger.info('Update API URL to: {}'.format(self.API))

    def _request(self, url, method='GET', data=None):
        res = requests.request(url=url, method=method, json=data)
        logging.debug('Request data: {}'.format(data))
        if not res.ok:
            res.raise_for_status()
        try:
            jr = res.json()
            logger.debug('JSON Response: {}'.format(jr))
            response = api_response.HueApiResponse.factory(jr)
            if type(response) is api_response.HueErrorResponse:
                raise HueError(response.description)
            return response
        except JSONDecodeError:
            logger.error(
                'Failed to decode JSON from response: {}'.format(res.text)
            )

    @staticmethod
    def from_url(url, username=None):
        from urlparse import urlparse
        res = urlparse(url)
        return Bridge(hostname=res.hostname, port=res.port, username=username)

    @staticmethod
    def discover(username=None, guess=True, fast=True):
        nupnp = Bridge.discover_nupnp(username=username)
        if not fast or not nupnp:
            upnp = Bridge.discover_upnp(username=username)
        else:
            logger.info('Skip UPNP discovery')
            upnp = []
        bridges = list(set(upnp + nupnp))
        return bridges[0] if guess and len(bridges) > 0 else bridges

    @staticmethod
    def discover_nupnp(username=None):
        r = requests.get('https://www.meethue.com/api/nupnp')
        return [Bridge(x['internalipaddress'], username) for x in r.json()]

    @staticmethod
    def discover_upnp(username=None):
        from netdisco.ssdp import scan

        PHILIPS = 'Royal Philips Electronics'
        BRIDGE_MODELS = ['929000226503', 'BSB002']

        hue_bridges = []
        res = scan()

        for h in res:
            logger.info('Check {}'.format(h))
            device_info = h.description.get('device', None)
            if device_info:
                manufacturer = device_info.get('manufacturer', None)
                model = device_info.get('modelNumber', None)
                if manufacturer == PHILIPS and model in BRIDGE_MODELS:
                    url = h.description['URLBase']
                    if url not in hue_bridges:
                        hue_bridges.append(url)
            else:
                logger.error('Missing device info')
        return [Bridge.from_url(x, username) for x in hue_bridges]

    def create_user(self, devicetype='zhue.py#user'):
        url = 'http://{}:{}/api'.format(self.hostname, self.port)
        data = {'devicetype': devicetype}
        res = self._request(method='POST', url=url, data=data)
        self.username = res._json['username']
        return res

    def delete_user(self, username):
        url = '{}/config/whitelist/{}'.format(self.API, username)
        return self._request(method='DELETE', url=url)

    def get_full_state(self):
        return self._request(method='GET', url=self.API)

    def _property(self, prop_url):
        url = '{}/{}'.format(self.API, prop_url)
        res = self._request(url)
        return res._json

    @property
    def config(self):
        return config.BridgeConfig(self, self._property('config'))

    @property
    def users(self):
        return self.config.users

    @property
    def lights(self):
        l = []
        for i, v in self._property('lights').iteritems():
            l.append(light.Light(self, i, v))
        return l

    @property
    def groups(self):
        l = []
        for i, v in self._property('groups').iteritems():
            l.append(group.Group(self, i, v))
        return l

    @property
    def schedules(self):
        l = []
        for i, v in self._property('schedules').iteritems():
            l.append(schedule.Schedule(self, i, v))
        return l

    @property
    def sensors(self):
        s = []
        for i, v in self._property('sensors').iteritems():
            s.append(sensor.factory(self, i, v))
        return s

    @property
    def scenes(self):
        s = []
        for i, v in self._property('scenes').iteritems():
            s.append(scene.Scene(self, i, v))
        return s

    def __regroup_sensors(self):
        d = {}
        for s in self.sensors:
            mac_addr = None
            try:
                mac_addr = s.mac_address
            except:
                pass
            if mac_addr in d:
                d[mac_addr].append(s)
            else:
                d[mac_addr] = [s]
        return d

    @property
    def devices(self):
        devs = []
        sensors_grouped = self.__regroup_sensors()
        for d in filter(None, sensors_grouped):
            devs.append(device.HueDevice.factory(sensors_grouped[d]))
        devs += self.lights
        return devs

    def __get_sensors_by_type(self, sensor_type):
        return [x for x in self.sensors if isinstance(x, sensor_type)]

    @property
    def temperature_sensors(self):
        return self.__get_sensors_by_type(sensor.TemperatureSensor)

    @property
    def light_level_sensors(self):
        return self.__get_sensors_by_type(sensor.LightLevelSensor)

    def __get_hue_objects_by_type(self, device_type):
        if device_type == 'sensor':
            return self.sensors
        elif device_type == 'light':
            return self.lights
        elif device_type == 'group':
            return self.groups
        elif device_type == 'schedule':
            return self.schedules
        elif device_type == 'scene':
            return self.scenes
        elif device_type == 'user':
            return self.users
        else:
            logger.error('Unknown device type')

    def __get_hue_object(self, device_type, name=None, hue_id=None,
                         exact=False):
        assert name or hue_id, 'Name or Hue ID must be provided'
        for d in self.__get_hue_objects_by_type(device_type):
            if hue_id and str(d.hue_id) == str(hue_id):
                return d
            if name:
                if exact:
                    if d.name == name:
                        return d
                else:
                    if re.match('.*{}.*'.format(name), d.name, re.IGNORECASE):
                        return d
        raise HueError('No matching item was found')

    def group(self, name=None, hue_id=None, exact=False):
        if hue_id == 0:
            return group.MasterGroup(self)
        return self.__get_hue_object('group', name, hue_id, exact)

    def light(self, name=None, hue_id=None, exact=False):
        return self.__get_hue_object('light', name, hue_id, exact)

    def sensor(self, name=None, hue_id=None, exact=False):
        return self.__get_hue_object('sensor', name, hue_id, exact)

    def schedule(self, name=None, hue_id=None, exact=False):
        return self.__get_hue_object('schedule', name, hue_id, exact)

    def scene(self, name=None, hue_id=None, exact=False):
        return self.__get_hue_object('scene', name, hue_id, exact)

    def user(self, name=None, username=None, exact=False):
        return self.__get_hue_object('user', name, username, exact)

    # Hue object discovery
    def __find_new(self, hueobjecttype):
        '''
        Starts a search for new Hue objects
        '''
        assert hueobjecttype in ['lights', 'sensors'], \
            'Unsupported object type {}'.format(hueobjecttype)
        url = '{}/{}'.format(self.API, hueobjecttype)
        return self._request(
            method='POST',
            url=url
        )

    def __get_new(self, hueobjecttype):
        '''
        Get a list of newly found Hue object
        '''
        assert hueobjecttype in ['lights', 'sensors'], \
            'Unsupported object type {}'.format(hueobjecttype)
        url = '{}/{}/new'.format(self.API, hueobjecttype)
        return self._request(url=url)

    def find_new_lights(self):
        return self.__find_new('lights')

    def new_lights(self):
        return self.__get_new('lights')

    def find_new_sensors(self):
        return self.__find_new('sensors')

    def new_sensors(self):
        return self.__get_new('sensors')

    # All on/off
    def all_off(self):
        return group.MasterGroup(self).off()

    def all_on(self):
        return group.MasterGroup(self).on()

    # Factory methods. Create new objects
    def create_schedule(self, *args, **kwargs):
        return schedule.Schedule.new(self, *args, **kwargs)

    # Software update
    def update_check(self):
        return
