# -*- coding: utf-8 -*-
#
#   Star Trek: Interstellar Transport
#
#                                Written in 2021 by Moky <<EMAIL>>
#
# ==============================================================================
# MIT License
#
# Copyright (c) 2021 <NAME>
#
# 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 time
import weakref
from abc import ABC
from typing import List, Dict, Set, Any, Optional

from .port import Arrival, Departure


class DepartureShip(Departure, ABC):

    # Departure task will be expired after 2 minutes if no response received
    EXPIRES = 120  # seconds

    # Departure task will be retried 2 times if response timeout
    RETRIES = 2

    # if (max_tries == -1),
    # means this ship will be sent only once
    # and no need to wait for response.
    DISPOSABLE = -1

    def __init__(self, priority: int = 0, max_tries: int = 3):  # max_tries = 1 + RETRIES
        super().__init__()
        # ship priority
        self.__priority = priority
        # expired time (timestamp in seconds)
        self.__expired = 0
        # tries:
        #   -1, this ship needs no response, so it will be sent out
        #       and removed immediately;
        #    0, this ship was sent and now is waiting for response,
        #       it should be removed after expired;
        #   >0, this ship needs retry and waiting for response,
        #       don't remove it now.
        self.__tries = max_tries

    @property
    def priority(self) -> int:
        return self.__priority

    #
    #   task states
    #

    # Override
    def is_new(self) -> bool:
        return self.__expired == 0

    # Override
    def is_disposable(self) -> bool:
        return self.__tries <= 0  # -1

    # Override
    def is_timeout(self, now: float) -> bool:
        return self.__tries > 0 and now > self.__expired

    # Override
    def is_failed(self, now: float) -> bool:
        return self.__tries == 0 and now > self.__expired

    # Override
    def touch(self, now: float):
        assert self.__tries > 0, 'touch error, tries=%d' % self.__tries
        # update retried time
        self.__expired = now + self.EXPIRES
        # decrease counter
        self.__tries -= 1


class DepartureHall:
    """ Memory cache for Departures """

    def __init__(self):
        super().__init__()
        self.__priorities: List[int] = []
        self.__fleets: Dict[int, List[Departure]] = {}  # priority => List[Departure]
        self.__map = weakref.WeakValueDictionary()  # sn => Departure
        self.__finished_times: Dict[Any, float] = {}  # sn => timestamp

    def append_departure(self, ship: Departure) -> bool:
        """
        Append outgoing ship to a fleet with priority

        :param ship: departure task carrying data package/fragment
        :return False on duplicated
        """
        priority = ship.priority
        # 1. choose an array with priority
        fleet = self.__fleets.get(priority)
        if fleet is None:
            # 1.1. create new array for this priority
            fleet = []
            self.__fleets[priority] = fleet
            # 1.2. insert the priority in a sorted list
            self.__insert(priority=priority)
        elif ship in fleet:
            # 1.3. check duplicated
            return False
        # 2. append to the tail
        fleet.append(ship)
        # 3. build mapping if SN exists
        sn = ship.sn
        if not (sn is None or ship.is_disposable()):
            # disposable ship needs no response, so
            # we don't build index for it.
            self.__map[sn] = ship
        return True

    def __insert(self, priority: int) -> bool:
        index = 0
        for value in self.__priorities:
            if value == priority:
                # duplicated
                return False
            elif value > priority:
                # got it
                break
            else:
                # current value is smaller than the new value,
                # keep going
                index += 1
        # insert new value before the bigger one
        self.__priorities.insert(index, priority)
        return True

    def check_response(self, ship: Arrival) -> Optional[Departure]:
        """
        Check response from income ship

        :param ship: incoming ship with SN
        :return finished task
        """
        sn = ship.sn
        assert sn is not None, 'SN not found: %s' % ship
        # check whether this task has already finished
        timestamp = self.__finished_times.get(sn)
        if timestamp is not None and timestamp > 0:
            return None
        # check departure task
        outgo = self.__map.get(sn, None)
        if outgo is None:
            return None
        # assert isinstance(outgo, Departure), 'outgo task error: %s' % outgo
        if outgo.check_response(ship=ship):
            # all fragments sent, departure task finished
            # remove it and clear mapping when SN exists
            self.__remove(ship=outgo, sn=sn)
            # mark finished time
            self.__finished_times[sn] = time.time()
            return outgo

    def __remove(self, ship: Departure, sn):
        priority = ship.priority
        fleet = self.__fleets.get(priority)
        if fleet is not None and ship in fleet:
            fleet.remove(ship)
            # remove array when empty
            if len(fleet) == 0:
                self.__fleets.pop(priority, None)
        # remove mapping by SN
        self.__map.pop(sn, None)

    def next_departure(self, now: float) -> Optional[Departure]:
        """
        Get next new/timeout task

        :param now: current time
        :return departure task
        """
        # task.__expired == 0
        task = self.__next_new_departure(now=now)
        if task is None:
            # task.__tries > 0 and timeout
            task = self.__next_timeout_departure(now=now)
        return task

    def __next_new_departure(self, now: float) -> Optional[Departure]:
        priorities = list(self.__priorities)
        for prior in priorities:
            # 1. get tasks with priority
            fleet = self.__fleets.get(prior)
            if fleet is None:
                continue
            # 2. seeking new task in this priority
            departures = list(fleet)
            for ship in departures:
                if ship.is_new():
                    if ship.is_disposable():
                        # disposable ship needs no response,
                        # remove it immediately.
                        fleet.remove(ship)
                        # TODO: disposable ship will not be mapped.
                        #       see 'append_departure(ship)'
                        sn = ship.sn
                        if sn is not None:
                            self.__map.pop(sn, None)
                    else:
                        # first time to try, update expired time for response
                        ship.touch(now=now)
                    return ship

    def __next_timeout_departure(self, now: float) -> Optional[Departure]:
        priorities = list(self.__priorities)
        for prior in priorities:
            # 1. get tasks with priority
            fleet = self.__fleets.get(prior)
            if fleet is None:
                continue
            # 2. seeking timeout task in this priority
            departures = list(fleet)
            for ship in departures:
                if ship.is_timeout(now=now):
                    # response timeout, needs retry now.
                    # 2.1. update expired time;
                    ship.touch(now=now)
                    # 2.2. move to the tail
                    if len(fleet) > 1:
                        fleet.remove(ship)
                        fleet.append(ship)
                    return ship
                elif ship.is_failed(now=now):
                    # try too many times and still missing response,
                    # task failed, remove this ship.
                    fleet.remove(ship)
                    sn = ship.sn
                    if sn is not None:
                        self.__map.pop(sn, None)
                    return ship

    def __clear(self, fleet: List[Departure], failed_tasks: Set[Departure], priority: int):
        # remove expired tasks
        for ship in failed_tasks:
            fleet.remove(ship)
            # remove mapping when SN exists
            sn = ship.sn
            if sn is not None:
                self.__map.pop(sn, None)
            # TODO: callback?
        # remove array when empty
        if len(fleet) == 0:
            self.__fleets.pop(priority, None)

    def purge(self):
        """ Clear all expired tasks """
        failed_tasks: Set[Departure] = set()
        now = time.time()
        priorities = list(self.__priorities)
        for prior in priorities:
            # 0. get tasks with priority
            fleet = self.__fleets.get(prior)
            if fleet is None:
                continue
            # 1. seeking expired task in this priority
            departures = list(fleet)
            for ship in departures:
                if ship.is_failed(now=now):
                    # task expired
                    failed_tasks.add(ship)
            # 2. clear expired tasks
            self.__clear(fleet=fleet, failed_tasks=failed_tasks, priority=prior)
            failed_tasks.clear()
        # 3. seeking neglected finished times
        neglected_times = set()
        ago = now - 3600
        keys = set(self.__finished_times.keys())
        for sn in keys:
            when = self.__finished_times.get(sn)
            if when is None or when < ago:
                # long time ago
                neglected_times.add(sn)
        # 4. clear neglected times
        for sn in neglected_times:
            self.__finished_times.pop(sn, None)
            self.__map.pop(sn, None)

# -*- coding: utf-8 -*-
#
#   Star Trek: Interstellar Transport
#
#                                Written in 2021 by Moky <<EMAIL>>
#
# ==============================================================================
# MIT License
#
# Copyright (c) 2021 <NAME>
#
# 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 time
import weakref
from abc import ABC
from typing import List, Dict, Set, Any, Optional

from .port import Arrival, Departure


class DepartureShip(Departure, ABC):

    # Departure task will be expired after 2 minutes if no response received
    EXPIRES = 120  # seconds

    # Departure task will be retried 2 times if response timeout
    RETRIES = 2

    # if (max_tries == -1),
    # means this ship will be sent only once
    # and no need to wait for response.
    DISPOSABLE = -1

    def __init__(self, priority: int = 0, max_tries: int = 3):  # max_tries = 1 + RETRIES
        super().__init__()
        # ship priority
        self.__priority = priority
        # expired time (timestamp in seconds)
        self.__expired = 0
        # tries:
        #   -1, this ship needs no response, so it will be sent out
        #       and removed immediately;
        #    0, this ship was sent and now is waiting for response,
        #       it should be removed after expired;
        #   >0, this ship needs retry and waiting for response,
        #       don't remove it now.
        self.__tries = max_tries

    @property
    def priority(self) -> int:
        return self.__priority

    #
    #   task states
    #

    # Override
    def is_new(self) -> bool:
        return self.__expired == 0

    # Override
    def is_disposable(self) -> bool:
        return self.__tries <= 0  # -1

    # Override
    def is_timeout(self, now: float) -> bool:
        return self.__tries > 0 and now > self.__expired

    # Override
    def is_failed(self, now: float) -> bool:
        return self.__tries == 0 and now > self.__expired

    # Override
    def touch(self, now: float):
        assert self.__tries > 0, 'touch error, tries=%d' % self.__tries
        # update retried time
        self.__expired = now + self.EXPIRES
        # decrease counter
        self.__tries -= 1


class DepartureHall:
    """ Memory cache for Departures """

    def __init__(self):
        super().__init__()
        self.__priorities: List[int] = []
        self.__fleets: Dict[int, List[Departure]] = {}  # priority => List[Departure]
        self.__map = weakref.WeakValueDictionary()  # sn => Departure
        self.__finished_times: Dict[Any, float] = {}  # sn => timestamp

    def append_departure(self, ship: Departure) -> bool:
        """
        Append outgoing ship to a fleet with priority

        :param ship: departure task carrying data package/fragment
        :return False on duplicated
        """
        priority = ship.priority
        # 1. choose an array with priority
        fleet = self.__fleets.get(priority)
        if fleet is None:
            # 1.1. create new array for this priority
            fleet = []
            self.__fleets[priority] = fleet
            # 1.2. insert the priority in a sorted list
            self.__insert(priority=priority)
        elif ship in fleet:
            # 1.3. check duplicated
            return False
        # 2. append to the tail
        fleet.append(ship)
        # 3. build mapping if SN exists
        sn = ship.sn
        if not (sn is None or ship.is_disposable()):
            # disposable ship needs no response, so
            # we don't build index for it.
            self.__map[sn] = ship
        return True

    def __insert(self, priority: int) -> bool:
        index = 0
        for value in self.__priorities:
            if value == priority:
                # duplicated
                return False
            elif value > priority:
                # got it
                break
            else:
                # current value is smaller than the new value,
                # keep going
                index += 1
        # insert new value before the bigger one
        self.__priorities.insert(index, priority)
        return True

    def check_response(self, ship: Arrival) -> Optional[Departure]:
        """
        Check response from income ship

        :param ship: incoming ship with SN
        :return finished task
        """
        sn = ship.sn
        assert sn is not None, 'SN not found: %s' % ship
        # check whether this task has already finished
        timestamp = self.__finished_times.get(sn)
        if timestamp is not None and timestamp > 0:
            return None
        # check departure task
        outgo = self.__map.get(sn, None)
        if outgo is None:
            return None
        # assert isinstance(outgo, Departure), 'outgo task error: %s' % outgo
        if outgo.check_response(ship=ship):
            # all fragments sent, departure task finished
            # remove it and clear mapping when SN exists
            self.__remove(ship=outgo, sn=sn)
            # mark finished time
            self.__finished_times[sn] = time.time()
            return outgo

    def __remove(self, ship: Departure, sn):
        priority = ship.priority
        fleet = self.__fleets.get(priority)
        if fleet is not None and ship in fleet:
            fleet.remove(ship)
            # remove array when empty
            if len(fleet) == 0:
                self.__fleets.pop(priority, None)
        # remove mapping by SN
        self.__map.pop(sn, None)

    def next_departure(self, now: float) -> Optional[Departure]:
        """
        Get next new/timeout task

        :param now: current time
        :return departure task
        """
        # task.__expired == 0
        task = self.__next_new_departure(now=now)
        if task is None:
            # task.__tries > 0 and timeout
            task = self.__next_timeout_departure(now=now)
        return task

    def __next_new_departure(self, now: float) -> Optional[Departure]:
        priorities = list(self.__priorities)
        for prior in priorities:
            # 1. get tasks with priority
            fleet = self.__fleets.get(prior)
            if fleet is None:
                continue
            # 2. seeking new task in this priority
            departures = list(fleet)
            for ship in departures:
                if ship.is_new():
                    if ship.is_disposable():
                        # disposable ship needs no response,
                        # remove it immediately.
                        fleet.remove(ship)
                        # TODO: disposable ship will not be mapped.
                        #       see 'append_departure(ship)'
                        sn = ship.sn
                        if sn is not None:
                            self.__map.pop(sn, None)
                    else:
                        # first time to try, update expired time for response
                        ship.touch(now=now)
                    return ship

    def __next_timeout_departure(self, now: float) -> Optional[Departure]:
        priorities = list(self.__priorities)
        for prior in priorities:
            # 1. get tasks with priority
            fleet = self.__fleets.get(prior)
            if fleet is None:
                continue
            # 2. seeking timeout task in this priority
            departures = list(fleet)
            for ship in departures:
                if ship.is_timeout(now=now):
                    # response timeout, needs retry now.
                    # 2.1. update expired time;
                    ship.touch(now=now)
                    # 2.2. move to the tail
                    if len(fleet) > 1:
                        fleet.remove(ship)
                        fleet.append(ship)
                    return ship
                elif ship.is_failed(now=now):
                    # try too many times and still missing response,
                    # task failed, remove this ship.
                    fleet.remove(ship)
                    sn = ship.sn
                    if sn is not None:
                        self.__map.pop(sn, None)
                    return ship

    def __clear(self, fleet: List[Departure], failed_tasks: Set[Departure], priority: int):
        # remove expired tasks
        for ship in failed_tasks:
            fleet.remove(ship)
            # remove mapping when SN exists
            sn = ship.sn
            if sn is not None:
                self.__map.pop(sn, None)
            # TODO: callback?
        # remove array when empty
        if len(fleet) == 0:
            self.__fleets.pop(priority, None)

    def purge(self):
        """ Clear all expired tasks """
        failed_tasks: Set[Departure] = set()
        now = time.time()
        priorities = list(self.__priorities)
        for prior in priorities:
            # 0. get tasks with priority
            fleet = self.__fleets.get(prior)
            if fleet is None:
                continue
            # 1. seeking expired task in this priority
            departures = list(fleet)
            for ship in departures:
                if ship.is_failed(now=now):
                    # task expired
                    failed_tasks.add(ship)
            # 2. clear expired tasks
            self.__clear(fleet=fleet, failed_tasks=failed_tasks, priority=prior)
            failed_tasks.clear()
        # 3. seeking neglected finished times
        neglected_times = set()
        ago = now - 3600
        keys = set(self.__finished_times.keys())
        for sn in keys:
            when = self.__finished_times.get(sn)
            if when is None or when < ago:
                # long time ago
                neglected_times.add(sn)
        # 4. clear neglected times
        for sn in neglected_times:
            self.__finished_times.pop(sn, None)
            self.__map.pop(sn, None)

# -*- coding: utf-8 -*- # #   Star Trek: Interstellar Transport # #                                Written in 2021 by Moky <<EMAIL>> # # ============================================================================== # MIT License # # Copyright (c) 2021 <NAME> # # 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. # ============================================================================== # Departure task will be expired after 2 minutes if no response received # seconds # Departure task will be retried 2 times if response timeout # if (max_tries == -1), # means this ship will be sent only once # and no need to wait for response. # max_tries = 1 + RETRIES # ship priority # expired time (timestamp in seconds) # tries: #   -1, this ship needs no response, so it will be sent out #       and removed immediately; #    0, this ship was sent and now is waiting for response, #       it should be removed after expired; #   >0, this ship needs retry and waiting for response, #       don't remove it now. # #   task states # # Override # Override # -1 # Override # Override # Override # update retried time # decrease counter Memory cache for Departures # priority => List[Departure] # sn => Departure # sn => timestamp Append outgoing ship to a fleet with priority          :param ship: departure task carrying data package/fragment         :return False on duplicated # 1. choose an array with priority # 1.1. create new array for this priority # 1.2. insert the priority in a sorted list # 1.3. check duplicated # 2. append to the tail # 3. build mapping if SN exists # disposable ship needs no response, so # we don't build index for it. # duplicated # got it # current value is smaller than the new value, # keep going # insert new value before the bigger one Check response from income ship          :param ship: incoming ship with SN         :return finished task # check whether this task has already finished # check departure task # assert isinstance(outgo, Departure), 'outgo task error: %s' % outgo # all fragments sent, departure task finished # remove it and clear mapping when SN exists # mark finished time # remove array when empty # remove mapping by SN Get next new/timeout task          :param now: current time         :return departure task # task.__expired == 0 # task.__tries > 0 and timeout # 1. get tasks with priority # 2. seeking new task in this priority # disposable ship needs no response, # remove it immediately. # TODO: disposable ship will not be mapped. #       see 'append_departure(ship)' # first time to try, update expired time for response # 1. get tasks with priority # 2. seeking timeout task in this priority # response timeout, needs retry now. # 2.1. update expired time; # 2.2. move to the tail # try too many times and still missing response, # task failed, remove this ship. # remove expired tasks # remove mapping when SN exists # TODO: callback? # remove array when empty Clear all expired tasks # 0. get tasks with priority # 1. seeking expired task in this priority # task expired # 2. clear expired tasks # 3. seeking neglected finished times # long time ago # 4. clear neglected times