# The MIT License (MIT)
#
# Copyright (c) 2015-2018 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import threading
import warnings
from ._ffi import EventType, Pose, VibrationType
from .utils import TimeoutManager
from .math import Vector, Quaternion


class DeviceListener(object):
  """
  Base class for device listeners -- objects that listen to Myo device events.
  """

  def on_event(self, event):
    if event.type.name:  # An event type that we know of.
      attr = 'on_' + event.type.name
      try:
        method = getattr(self, attr)
      except AttributeError:
        pass
      else:
        return method(event)

    warnings.warn('unhandled event: {}'.format(event))
    return True  # continue

  def on_paired(self, event): pass
  def on_unpaired(self, event): pass
  def on_connected(self, event): pass
  def on_disconnected(self, event): pass
  def on_arm_synced(self, event): pass
  def on_arm_unsynced(self, event): pass
  def on_unlocked(self, event): pass
  def on_locked(self, event): pass
  def on_pose(self, event): pass
  def on_orientation(self, event): pass
  def on_rssi(self, event): pass
  def on_battery_level(self, event): pass
  def on_emg(self, event): pass
  def on_warmup_completed(self, event): pass


class DeviceProxy(object):
  """
  Stateful container for Myo device data.
  """

  def __init__(self, device, timestamp, firmware_version, mac_address,
               condition_class=threading.Condition):
    self._device = device
    self._mac_address = mac_address
    self._cond = condition_class()
    self._pair_time = timestamp
    self._unpair_time = None
    self._connect_time = None
    self._disconnect_time = None
    self._emg = None
    self._orientation_update_index = 0
    self._orientation = Quaternion.identity()
    self._acceleration = Vector(0, 0, 0)
    self._gyroscope = Vector(0, 0, 0)
    self._pose = Pose.rest
    self._arm = None
    self._x_direction = None
    self._rssi = None
    self._battery_level = None
    self._firmware_version = firmware_version
    self._name = None

  def __repr__(self):
    with self._cond:
      con = 'connected' if self._connected else 'disconnected'
      return '<DeviceProxy ({}) name={!r}>'.format(con, self.name)

  @property
  def _connected(self):
    return self._connect_time is not None and self._disconnect_time is None

  @property
  def connected(self):
    with self._cond:
      return self._connected

  @property
  def paired(self):
    with self._cond:
      return self._unpair_time is not None

  @property
  def mac_address(self):
    return self._mac_address

  @property
  def pair_time(self):
    return self._pair_time

  @property
  def unpair_time(self):
    with self._cond:
      return self._unpair_time

  @property
  def connect_time(self):
    return self._connect_time

  @property
  def disconnect_time(self):
    with self._cond:
      return self._disconnect_time

  @property
  def firmware_version(self):
    return self._firmware_version

  @property
  def orientation_update_index(self):
    with self._cond:
      return self._orientation_update_index

  @property
  def orientation(self):
    with self._cond:
      return self._orientation.copy()

  @property
  def acceleration(self):
    with self._cond:
      return self._acceleration.copy()

  @property
  def gyroscope(self):
    with self._cond:
      return self._gyroscope.copy()

  @property
  def pose(self):
    with self._cond:
      return self._pose

  @property
  def arm(self):
    with self._cond:
      return self._arm

  @property
  def x_direction(self):
    with self._cond:
      return self._x_direction

  @property
  def rssi(self):
    with self._cond:
      return self._rssi

  @property
  def emg(self):
    with self._cond:
      return self._emg

  def set_locking_policy(self, policy):
    self._device.set_locking_policy(policy)

  def stream_emg(self, type):
    self._device.stream_emg(type)

  def vibrate(self, type=VibrationType.short):
    self._device.vibrate(type)

  def request_rssi(self):
    with self._cond:
      self._rssi = None
      self._device.request_rssi()

  def request_battery_level(self):
    with self._cond:
      self._battery_level = None
      self._device.request_battery_level()


class ApiDeviceListener(DeviceListener):

  def __init__(self, condition_class=threading.Condition):
    self._condition_class = condition_class
    self._cond = condition_class()
    self._devices = {}

  @property
  def devices(self):
    with self._cond:
      return list(self._devices.values())

  @property
  def connected_devices(self):
    with self._cond:
      return [x for x in self._devices.values() if x.connected]

  def wait_for_single_device(self, timeout=None, interval=0.5):
    """
    Waits until a Myo is was paired **and** connected with the Hub and returns
    it. If the *timeout* is exceeded, returns None. This function will not
    return a Myo that is only paired but not connected.

    # Parameters
    timeout: The maximum time to wait for a device.
    interval: The interval at which the function should exit sleeping. We can
      not sleep endlessly, otherwise the main thread can not be exit, eg.
      through a KeyboardInterrupt.
    """

    timer = TimeoutManager(timeout)
    with self._cond:
      # As long as there are no Myo's connected, wait until we
      # get notified about a change.
      while not timer.check():
        # Check if we found a Myo that is connected.
        for device in self._devices.values():
          if device.connected:
            return device
        self._cond.wait(timer.remainder(interval))

    return None

  def on_event(self, event):
    with self._cond:
      if event.type == EventType.paired:
        device = DeviceProxy(event.device, event.timestamp,
          event.firmware_version, self._condition_class)
        self._devices[device._device.handle] = device
        self._cond.notify_all()
        return
      else:
        try:
          if event.type == EventType.unpaired:
            device = self._devices.pop(event.device.handle)
          else:
            device = self._devices[event.device.handle]
        except KeyError:
          message = 'Myo device not in the device list ({})'
          warnings.warn(message.format(event), RuntimeWarning)
          return
      if event.type == EventType.unpaired:
        with device._cond:
          device._unpair_time = event.timestamp
        self._cond.notify_all()

    with device._cond:
      if event.type == EventType.connected:
        device._connect_time = event.timestamp
      elif event.type == EventType.disconnected:
        device._disconnect_time = event.timestamp
      elif event.type == EventType.emg:
        device._emg = event.emg
      elif event.type == EventType.arm_synced:
        device._arm = event.arm
        device._x_direction = event.x_direction
      elif event.type == EventType.rssi:
        device._rssi = event.rssi
      elif event.type == EventType.battery_level:
        device._battery_level = event.battery_level
      elif event.type == EventType.pose:
        device._pose = event.pose
      elif event.type == EventType.orientation:
        device._orientation_update_index += 1
        device._orientation = event.orientation
        device._gyroscope = event.gyroscope
        device._acceleration = event.acceleration