"""
Support for Hikvision event stream events represented as binary sensors.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.hikvision/
"""
import logging
from datetime import timedelta
import voluptuous as vol

from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.components.binary_sensor import (
    BinarySensorDevice, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
    CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
    CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
    ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)

REQUIREMENTS = ['pyhik==0.1.8']
_LOGGER = logging.getLogger(__name__)

CONF_IGNORED = 'ignored'
CONF_DELAY = 'delay'

DEFAULT_PORT = 80
DEFAULT_IGNORED = False
DEFAULT_DELAY = 0

ATTR_DELAY = 'delay'

DEVICE_CLASS_MAP = {
    'Motion': 'motion',
    'Line Crossing': 'motion',
    'Field Detection': 'motion',
    'Video Loss': None,
    'Tamper Detection': 'motion',
    'Shelter Alarm': None,
    'Disk Full': None,
    'Disk Error': None,
    'Net Interface Broken': 'connectivity',
    'IP Conflict': 'connectivity',
    'Illegal Access': None,
    'Video Mismatch': None,
    'Bad Video': None,
    'PIR Alarm': 'motion',
    'Face Detection': 'motion',
    'Scene Change Detection': 'motion',
    'I/O': None,
    'Unattended Baggage': 'motion',
    'Attended Baggage': 'motion',
    'Recording Failure': None,
}

CUSTOMIZE_SCHEMA = vol.Schema({
    vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean,
    vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int
    })

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_NAME): cv.string,
    vol.Required(CONF_HOST): cv.string,
    vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
    vol.Optional(CONF_SSL, default=False): cv.boolean,
    vol.Required(CONF_USERNAME): cv.string,
    vol.Required(CONF_PASSWORD): cv.string,
    vol.Optional(CONF_CUSTOMIZE, default={}):
        vol.Schema({cv.string: CUSTOMIZE_SCHEMA}),
})


def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up the Hikvision binary sensor devices."""
    name = config.get(CONF_NAME)
    host = config.get(CONF_HOST)
    port = config.get(CONF_PORT)
    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)

    customize = config.get(CONF_CUSTOMIZE)

    if config.get(CONF_SSL):
        protocol = 'https'
    else:
        protocol = 'http'

    url = '{}://{}'.format(protocol, host)

    data = HikvisionData(hass, url, port, name, username, password)

    if data.sensors is None:
        _LOGGER.error("Hikvision event stream has no data, unable to setup")
        return False

    entities = []

    for sensor, channel_list in data.sensors.items():
        for channel in channel_list:
            # Build sensor name, then parse customize config.
            if data.type == 'NVR':
                sensor_name = '{}_{}'.format(
                    sensor.replace(' ', '_'), channel[1])
            else:
                sensor_name = sensor.replace(' ', '_')

            custom = customize.get(sensor_name.lower(), {})
            ignore = custom.get(CONF_IGNORED)
            delay = custom.get(CONF_DELAY)

            _LOGGER.debug("Entity: %s - %s, Options - Ignore: %s, Delay: %s",
                          data.name, sensor_name, ignore, delay)
            if not ignore:
                entities.append(HikvisionBinarySensor(
                    hass, sensor, channel[1], data, delay))

    add_entities(entities)


class HikvisionData(object):
    """Hikvision device event stream object."""

    def __init__(self, hass, url, port, name, username, password):
        """Initialize the data object."""
        from pyhik.hikvision import HikCamera
        self._url = url
        self._port = port
        self._name = name
        self._username = username
        self._password = password

        # Establish camera
        self.camdata = HikCamera(
            self._url, self._port, self._username, self._password)

        if self._name is None:
            self._name = self.camdata.get_name

        hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
        hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_hik)

    def stop_hik(self, event):
        """Shutdown Hikvision subscriptions and subscription thread on exit."""
        self.camdata.disconnect()

    def start_hik(self, event):
        """Start Hikvision event stream thread."""
        self.camdata.start_stream()

    @property
    def sensors(self):
        """Return list of available sensors and their states."""
        return self.camdata.current_event_states

    @property
    def cam_id(self):
        """Return device id."""
        return self.camdata.get_id

    @property
    def name(self):
        """Return device name."""
        return self._name

    @property
    def type(self):
        """Return device type."""
        return self.camdata.get_type

    def get_attributes(self, sensor, channel):
        """Return attribute list for sensor/channel."""
        return self.camdata.fetch_attributes(sensor, channel)


class HikvisionBinarySensor(BinarySensorDevice):
    """Representation of a Hikvision binary sensor."""

    def __init__(self, hass, sensor, channel, cam, delay):
        """Initialize the binary_sensor."""
        self._hass = hass
        self._cam = cam
        self._sensor = sensor
        self._channel = channel

        if self._cam.type == 'NVR':
            self._name = '{} {} {}'.format(self._cam.name, sensor, channel)
        else:
            self._name = '{} {}'.format(self._cam.name, sensor)

        self._id = '{}.{}.{}'.format(self._cam.cam_id, sensor, channel)

        if delay is None:
            self._delay = 0
        else:
            self._delay = delay

        self._timer = None

        # Register callback function with pyHik
        self._cam.camdata.add_update_callback(self._update_callback, self._id)

    def _sensor_state(self):
        """Extract sensor state."""
        return self._cam.get_attributes(self._sensor, self._channel)[0]

    def _sensor_last_update(self):
        """Extract sensor last update time."""
        return self._cam.get_attributes(self._sensor, self._channel)[3]

    @property
    def name(self):
        """Return the name of the Hikvision sensor."""
        return self._name

    @property
    def unique_id(self):
        """Return a unique ID."""
        return self._id

    @property
    def is_on(self):
        """Return true if sensor is on."""
        return self._sensor_state()

    @property
    def device_class(self):
        """Return the class of this sensor, from DEVICE_CLASSES."""
        try:
            return DEVICE_CLASS_MAP[self._sensor]
        except KeyError:
            # Sensor must be unknown to us, add as generic
            return None

    @property
    def should_poll(self):
        """No polling needed."""
        return False

    @property
    def device_state_attributes(self):
        """Return the state attributes."""
        attr = {}
        attr[ATTR_LAST_TRIP_TIME] = self._sensor_last_update()

        if self._delay != 0:
            attr[ATTR_DELAY] = self._delay

        return attr

    def _update_callback(self, msg):
        """Update the sensor's state, if needed."""
        _LOGGER.debug('Callback signal from: %s', msg)

        if self._delay > 0 and not self.is_on:
            # Set timer to wait until updating the state
            def _delay_update(now):
                """Timer callback for sensor update."""
                _LOGGER.debug("%s Called delayed (%ssec) update",
                              self._name, self._delay)
                self.schedule_update_ha_state()
                self._timer = None

            if self._timer is not None:
                self._timer()
                self._timer = None

            self._timer = track_point_in_utc_time(
                self._hass, _delay_update,
                utcnow() + timedelta(seconds=self._delay))

        elif self._delay > 0 and self.is_on:
            # For delayed sensors kill any callbacks on true events and update
            if self._timer is not None:
                self._timer()
                self._timer = None

            self.schedule_update_ha_state()

        else:
            self.schedule_update_ha_state()
