#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2020 FABRIC Testbed
#
# 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.
#
#
# Author: Komal Thareja (kthare10@renci.org)
from __future__ import annotations

from abc import abstractmethod, ABC
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from fabric_cf.actor.core.apis.abc_tick import ABCTick


class ABCTicker(ABC):
    """
    ITicker defines the interface for the internal clock of a container. A container measures time in units
    of cycles. A cycle can have a variable length, which is specified at boot time.
    Container clocks can optionally be configured with an offset to be applied when calculating the current cycle.
    For more details about cycles see Time within the container can advance automatically, based on the internal
    computer clock, or manually, based on calls to the container clock's tick method.

    A clock implementation must support at least one mode of execution (manual or automatic), but it need not
    support both.

    A clock implementation maintains a collection of subscribers, which will be notified about the passage of time.
    Subscriptions are dynamic and can be changed at run time.

    Abstractly, each subscriber will receive a notification about the advancement of the container clock.
    Some implementations may guarantee that if an object has received a notification for cycle x, the next
    notification will be for cycle x+1. However, in general, no such guarantees are provided. As a matter of fact,
    code executing in real time will often "skip" cycles. Therefore, subscribed object, in general, must be able
    to handle correctly cases in which one or more clock cycle notifications are omitted.
    """

    @abstractmethod
    def tick(self):
        """
        Advances the clock with one cycle. This operation is only
        applicable for manual clocks. This is a blocking operation.
        """

    @abstractmethod
    def start(self):
        """
        Starts the clock.
        """

    @abstractmethod
    def stop(self):
        """
        Stops/pauses the clock.

        @throws Exception in case of error
        """

    @abstractmethod
    def add_tickable(self, *, tickable: ABCTick):
        """
        Adds an object to the list of objects subscribed to receive tick
        events.

        @param tickable object to add
        """

    @abstractmethod
    def remove_tickable(self, *, tickable: ABCTick):
        """
        Removes an object from the list of object subscribed to receive
        tick events.

        @param tickable object to remove
        """

    @abstractmethod
    def clear(self):
        """
        Stops the clock and unregisters all registered items.

        @throws Exception in case of error
        """

    @abstractmethod
    def get_current_cycle(self) -> int:
        """
        Returns the current clock cycle.

        @return current clock cycle
        """

    @abstractmethod
    def set_current_cycle(self, *, cycle: int):
        """
        Sets the current cycle. The first cycle to be generated by the
        clock will be cycle+1. This method can called only if
        initialize has not yet been called.

        @param cycle current cycle
        """

    @abstractmethod
    def set_beginning_of_time(self, *, value: int):
        """
        Sets the clock offset (relative to current time). Clock offset
        is measured in milliseconds. Clock offset is used to calculate the
        current clock cycle by subtracting it from current time and dividing by
        the length of a cycle. This method can called only if
        initialize has not yet been called.

        @param value clock offset in milliseconds
        """

    @abstractmethod
    def get_beginning_of_time(self) -> int:
        """
        Returns the clock offset. Clock offset is measured in
        milliseconds.

        @return clock offset in milliseconds
        """

    @abstractmethod
    def get_cycle_millis(self) -> int:
        """
        Returns the cycle length. Cycle length is measured in
        milliseconds.

        @return cycle length in milliseconds
        """

    @abstractmethod
    def set_cycle_millis(self, *, cycle_millis: int):
        """
        Sets the cycle length. Cycle length is measured in milliseconds.
        This method can called only if initialize has not yet been
        called.

        @param cycleMillis cycle length in milliseconds
        """

    @abstractmethod
    def is_manual(self) -> bool:
        """
        Checks if the clock implementation is manual, i.e., time
        advances manually as a result of calls to tick

        @return true if the clock is manual, false otherwise.
        """

    @abstractmethod
    def set_manual(self, *, value: bool):
        """
        Sets the manual clock flag. This method may throw a
        Exception if the clock does not support the
        particular execution mode. This method can called only if
        initialize has not yet been called.

        @param value value for the manual flag
        """
