"""The clock manager subsystem timestamps readings and implements timers.

No other controller subsystem has a concept of device time.  They are all
purely reactive to whatever work items are queued for them.  This is
by design since IOTile devices are meant to be event-driven rather than
polling based whenever possible.

However, there are times when you need to queue operations to be performed
periodically.  It is the clock manager subsystem's job to allow for queuing
these periodic tasks.  Internally, all timers work by just sending a
periodic input to the sensor graph subsystem.  There are 4 timer inputs
available:

- normal tick: generated every 10 seconds in all circumstances.
- fast tick: generated every 1 second when enabled.
- user tick 1: generated at a user configurable interval and easily
  updated dynamically using an RPC.
- user tick 2: generated at a user configurable interval and easily
  updated dynamically using an RPC.

The first two ticks are reserved for internal use and cannot be adjusted
by the user, hower the last two ticks are designed for easy control in
the field.


Device Time
===========

IOTile devices do not require a concept of UTC time in order to function.
Those device that include a properly synchronized realtime-clock (RTC) can
transparently gain the benefits of UTC time stamping on all of their generated
data, however low cost devices that do not have a proper RTC can also be
implemented using the device uptime as the source of timestamps rather than
a UTC value.

Internally, all timestamps in an IOTile device are represented to 1 second
precision using a uint32_t.  UTC timestamps are distinguished from uptime
timestamps by setting the MSB of the timestamps.  The epoch used for UTC
timestamps is Jan 1, 2000 00:00, i.e. the millenium, not the unix epoch.


Emulation Notes
===============

There are many cases when you do not want an emulated device to experience the
passage of time in the same way as a real device.  For example, you may want
to write test cases that inspect the timestamps generated by a device and so
you need control over what the device time will be.  You may also want to
perform a low level logic test where you verify what events will occur when a
specific timer input is sent to the sensor graph subsystem.  In this case
it is particularly important that you not have non-deterministic other inputs
being sent to sensor-graph depending on how long the test took to execute.

There are other cases where you want to run an end-to-end type test and want
to see the periodic data produced by an emulated device after X minutes or
hours have passed.

To handle both of these use cases, the emulated clock subsystem supports running
a device with time enable or disabled.  If time is disabled, the device's uptime
is frozen at a fixed value and no periodic inputs are generated to sensor-graph.

You are free to send the same periodic inputs to sensor-graph yourself using
rpcs if you wish to control the passage of emulation time (as seen by your
sensor-graph rules) in a fine-grained fashion.

Handling Time When Loading Snapshots
-----------------------------------

TODO: figure out a good set of options for resuming a device when utc time
      is set.
"""

from iotile.core.hw.virtual import tile_rpc
from ...constants import rpcs, pack_error, Error, ControllerSubsystem, streams
from .controller_system import ControllerSubsystemBase


class ClockManagerSubsystem(ControllerSubsystemBase):
    """Container state of the clock manager subsystem."""

    FAST_TICK = 0
    USER1_TICK = 1
    USER2_TICK = 2

    TICK_NAMES = {
        FAST_TICK: 'fast',
        USER1_TICK: 'user1',
        USER2_TICK: 'user2'
    }

    TICK_STREAMS = {
        'fast': streams.FAST_TICK,
        'normal': streams.NORMAL_TICK,
        'user1': streams.USER_TICK_1,
        'user2': streams.USER_TICK_2
    }

    def __init__(self, emulator, has_rtc=False):
        super(ClockManagerSubsystem, self).__init__(emulator)

        self.uptime = 0
        self.time_offset = 0
        self.is_utc = False

        self.stored_offset = None
        self.has_rtc = has_rtc
        self.ticks = dict(fast=0, user1=0, user2=0, normal=10)
        self.tick_counters = dict(fast=0, user1=0, user2=0, normal=0)

        # Hook for allowing us to link this subsystem to sensor_graph
        self.graph_input = lambda x, y: None

    def clear_to_reset(self, config_vars):
        """Clear all volatile information across a reset.

        The reset behavior is that:
        - uptime is reset to 0
        - is `has_rtc` is True, the utc_offset is preserved
        - otherwise the utc_offset is cleared to none
        """

        super(ClockManagerSubsystem, self).clear_to_reset(config_vars)

        self.tick_counters = dict(fast=0, user1=0, user2=0, normal=0)

        self.is_utc = False
        self.time_offset = 0

        if self.has_rtc and self.stored_offset is not None:
            self.time_offset = self.stored_offset + self.uptime

        self.uptime = 0

        self.ticks['fast'] = config_vars.get('fast_tick', 0)
        self.ticks['user1'] = config_vars.get('user_tick_1', 0)
        self.ticks['user2'] = config_vars.get('user_tick_2', 0)

    def tick_name(self, tick_index):
        """Convert a tick index into a string name."""

        return self.TICK_NAMES.get(tick_index)

    def handle_tick(self):
        """Internal callback every time 1 second has passed."""

        self.uptime += 1

        for name, interval in self.ticks.items():
            if interval == 0:
                continue

            self.tick_counters[name] += 1
            if self.tick_counters[name] == interval:
                self.graph_input(self.TICK_STREAMS[name], self.uptime)
                self.tick_counters[name] = 0

    def set_tick(self, index, interval):
        """Update the a tick's interval.

        Args:
            index (int): The index of the tick that you want to fetch.
            interval (int): The number of seconds between ticks.
                Setting this to 0 will disable the tick.

        Returns:
            int: An error code.
        """

        name = self.tick_name(index)
        if name is None:
            return pack_error(ControllerSubsystem.SENSOR_GRAPH, Error.INVALID_ARRAY_KEY)

        self.ticks[name] = interval
        return Error.NO_ERROR

    def get_tick(self, index):
        """Get a tick's interval.

        Args:
            index (int): The index of the tick that you want to fetch.

        Returns:
            int, int: Error code and The tick's interval in seconds.

            A value of 0 means that the tick is disabled.
        """

        name = self.tick_name(index)
        if name is None:
            return [pack_error(ControllerSubsystem.SENSOR_GRAPH, Error.INVALID_ARRAY_KEY), 0]

        return [Error.NO_ERROR, self.ticks[name]]

    def get_time(self, force_uptime=False):
        """Get the current UTC time or uptime.

        By default, this method will return UTC time if possible and fall back
        to uptime if not.  If you specify, force_uptime=True, it will always
        return uptime even if utc time is available.

        Args:
            force_uptime (bool): Always return uptime, defaults to False.

        Returns:
            int: The current uptime or encoded utc time.
        """

        if force_uptime:
            return self.uptime

        time = self.uptime + self.time_offset

        if self.is_utc:
            time |= (1 << 31)

        return time

    def synchronize_clock(self, offset):
        """Persistently synchronize the clock to UTC time.

        Args:
            offset (int): The number of seconds since 1/1/2000 00:00Z
        """

        self.time_offset = offset - self.uptime
        self.is_utc = True

        if self.has_rtc:
            self.stored_offset = self.time_offset


class ClockManagerMixin(object):
    """Mixin to add the clock manager subsystem into ReferenceController."""

    def __init__(self, emulator, has_rtc):
        self.clock_manager = ClockManagerSubsystem(emulator, has_rtc)

        self.declare_config_variable('fast_tick', 0x2000, 'uint32_t', default=0)
        self.declare_config_variable('user_tick_1', 0x2002, 'uint32_t', default=0)
        self.declare_config_variable('user_tick_2', 0x2003, 'uint32_t', default=0)

        self._post_config_subsystems.append(self.clock_manager)

    @tile_rpc(*rpcs.GET_USER_TIMER)
    def get_user_timer(self, index):
        """Get the current value of a user timer."""

        err, tick = self.clock_manager.get_tick(index)
        return [err, tick]

    @tile_rpc(*rpcs.SET_USER_TIMER)
    def set_user_timer(self, value, index):
        """Set the current value of a user timer."""

        err = self.clock_manager.set_tick(index, value)
        return [err]

    @tile_rpc(*rpcs.GET_CURRENT_TIME)
    def get_current_time(self):
        """Get the device's current time."""

        time = self.clock_manager.get_time()

        return [time]

    @tile_rpc(*rpcs.GET_CURRENT_UPTIME)
    def get_current_uptime(self):
        """Get the device's current time."""

        time = self.clock_manager.get_time(force_uptime=True)

        return [time]

    @tile_rpc(*rpcs.GET_UTC_TIME_OFFSET)
    def get_time_offset(self):
        """Get the currently programmed time offset."""

        return [self.clock_manager.time_offset, int(self.clock_manager.is_utc)]

    @tile_rpc(*rpcs.SET_UTC_TIME_OFFSET)
    def set_time_offset(self, offset, is_utc):
        """Temporarily set the current time offset."""

        is_utc = bool(is_utc)
        self.clock_manager.time_offset = offset
        self.clock_manager.is_utc = is_utc

        return [Error.NO_ERROR]

    @tile_rpc(*rpcs.SYNCHRONIZE_CLOCK)
    def synchronize_clock(self, offset):
        """Persistently synchronize the device's clock to UTC."""

        self.clock_manager.synchronize_clock(offset)

        return [Error.NO_ERROR]
