#
# uchroma - Copyright (C) 2017 Steve Kondik
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#

# pylint: disable=no-member, invalid-name

import asyncio
import functools

from concurrent import futures

import evdev

from uchroma.util import ensure_future


class InputManager:
    """
    Manages event devices associated with a physical device instance and
    allows for callback registration. Reader loop is fully asynchronous.
    See the InputQueue class for a higher level API.
    """
    def __init__(self, driver, input_devices: list):

        self._driver = driver
        self._input_devices = input_devices
        self._event_devices = []
        self._event_callbacks = []

        self._logger = driver.logger

        self._opened = False
        self._closing = False

        self._tasks = []


    async def _evdev_callback(self, device):
        async for event in device.async_read_loop():
            try:
                if not self._opened:
                    return

                if event.type == evdev.ecodes.EV_KEY:
                    ev = evdev.categorize(event)

                    for callback in self._event_callbacks:
                        await callback(ev)

                if not self._opened:
                    return

            except (OSError, IOError) as err:
                self._logger.exception("Event device error", exc_info=err)
                break


    def _evdev_close(self, event_device, future):
        self._logger.info('Closing event device %s', event_device)


    def _open_input_devices(self):
        if self._opened:
            return True

        for input_device in self._input_devices:
            try:
                event_device = evdev.InputDevice(input_device)
                self._event_devices.append(event_device)

                task = ensure_future(self._evdev_callback(event_device))
                task.add_done_callback(functools.partial(self._evdev_close, event_device))
                self._tasks.append(task)

                self._logger.info('Opened event device %s', event_device)

            except Exception as err:
                self._logger.exception("Failed to open device: %s", input_device, exc_info=err)

        if self._event_devices:
            self._opened = True

        return self._opened


    async def _close_input_devices(self):
        if not hasattr(self, '_opened') or not self._opened:
            return

        self._opened = False

        for event_device in self._event_devices:
            asyncio.get_event_loop().remove_reader(event_device.fileno())
            event_device.close()

        tasks = []
        for task in self._tasks:
            if not task.done():
                task.cancel()
                tasks.append(task)

        await asyncio.wait(tasks, return_when=futures.ALL_COMPLETED)
        self._event_devices.clear()


    def add_callback(self, callback) -> bool:
        """
        Add a new callback (coroutine) which will fire when new
        input events are received.

        :param callback: coroutine to add
        :return: True if successful
        """
        if callback in self._event_callbacks:
            return True

        if not self._opened:
            if not self._open_input_devices():
                return False

        self._event_callbacks.append(callback)
        return True


    async def remove_callback(self, callback):
        """
        Removes a previously registered callback

        :param callback: coroutine to remove
        """
        if callback not in self._event_callbacks:
            return

        self._event_callbacks.remove(callback)

        if not self._event_callbacks:
            await self._close_input_devices()


    async def shutdown(self):
        """
        Shuts down the InputManager and disconnects any active callbacks
        """
        for callback in self._event_callbacks:
            await ensure_future(self.remove_callback(callback))


    def grab(self, excl: bool):
        """
        Get exclusive access to the device

        WARNING: Calling this on your primary input device might
        cause you a bad day (or at least a reboot)! Use with devices
        (like keypads) where we don't want other apps to see actual
        scancodes.

        :param excl: True to gain exclusive access, False to release
        """
        if not self._opened:
            return

        for event_device in self._event_devices:
            if excl:
                event_device.grab()
            else:
                event_device.ungrab()


    @property
    def input_devices(self):
        """
        List of input devices associated with the parent device
        """
        return self._input_devices


    def __del__(self):
        self.shutdown()