# -*- 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 abstractmethod
from typing import Optional, List

from .types import AddressPairObject
from .net import Connection, Hub
from .port import Arrival, Departure
from .port import Docker, Gate, GateDelegate

from .dock import Dock, LockedDock


class StarDocker(AddressPairObject, Docker):
    """
        Star Docker
        ~~~~~~~~~~~

        @abstract properties:
            connection()
            delegate()

        @abstract methods:
            - pack(payload, priority)
            - heartbeat()
            - _get_arrival(data)
            - _check_arrival(ship)
    """

    def __init__(self, remote: tuple, local: Optional[tuple], gate: Gate):
        super().__init__(remote=remote, local=local)
        self.__gate = weakref.ref(gate)
        self.__dock = self._create_dock()
        # remaining data to be sent
        self.__last_outgo: Optional[Departure] = None
        self.__last_fragments: List[bytes] = []

    # noinspection PyMethodMayBeStatic
    def _create_dock(self) -> Dock:
        """ Override for user-customized dock """
        return LockedDock()

    @property  # protected
    def gate(self) -> Gate:
        return self.__gate()

    @property  # protected
    def hub(self) -> Optional[Hub]:
        raise NotImplemented

    @property  # Override
    def remote_address(self) -> tuple:  # (str, int)
        return self._remote

    @property  # Override
    def local_address(self) -> Optional[tuple]:  # (str, int)
        return self._local

    @property  # Override
    def connection(self) -> Optional[Connection]:
        gate = self.gate
        if gate is not None:
            return gate.get_connection(remote=self._remote, local=self._local)

    @property  # Override
    def delegate(self) -> Optional[GateDelegate]:
        gate = self.gate
        if gate is not None:
            return gate.delegate

    # Override
    def process(self) -> bool:
        # 1. get connection which is ready for sending data
        conn = self.connection
        if conn is None or not conn.alive:
            # connection not ready now
            return False
        # 2. get data to be sent
        if len(self.__last_fragments) > 0:
            # get remaining fragments from last outgo task
            outgo = self.__last_outgo
            fragments = self.__last_fragments
            self.__last_outgo = None
            self.__last_fragments = []
        else:
            # get next outgo task
            now = int(time.time())
            outgo = self._next_departure(now=now)
            if outgo is None:
                # nothing to do now, return False to let the thread have a rest
                return False
            elif outgo.is_failed(now=now):
                # task timeout, return True to process next one
                error = TimeoutError('Request timeout')
                self.__on_error(error=error, ship=outgo, connection=conn)
                return True
            else:
                # get fragments from outgo task
                fragments = outgo.fragments
                if len(fragments) == 0:
                    # all fragments of this task have been sent already
                    # return True to process next one
                    return True
        # 3. process fragments of outgo task
        index = 0
        sent = 0
        try:
            remote_address = self.remote_address
            for fra in fragments:
                sent = conn.send(data=fra, target=remote_address)
                if sent < len(fra):
                    # buffer overflow?
                    break
                else:
                    index += 1
            fra_cnt = len(fragments)
            if index < fra_cnt:
                # task failed
                error = ConnectionError('only %d/%d fragments sent' % (index, fra_cnt))
            else:
                # task done
                return True
        except Exception as e:
            # socket error, callback
            error = e
        # remove sent fragments
        while index > 0:
            fragments.pop(0)
            index -= 1
        # remove partially sent data of next fragment
        if sent > 0:
            last = fragments.pop(0)
            fragments.insert(0, last[sent:])
        # store remaining data
        self.__last_outgo = outgo
        self.__last_fragments = fragments
        # callback for error
        self.__on_error(error=error, ship=outgo, connection=conn)

    def __on_error(self, error, ship: Departure, connection: Connection):
        # callback for error
        delegate = self.delegate
        if delegate is not None:
            remote = self.remote_address
            local = self.local_address
            delegate.gate_error(error=error, ship=ship, source=local, destination=remote, connection=connection)

    # Override
    def process_received(self, data: bytes):
        # 1. get income ship from received data
        income = self._get_arrival(data=data)
        if income is None:
            return None
        # 2. check income ship for response
        income = self._check_arrival(ship=income)
        if income is None:
            return None
        # 3. callback for processing income ship with completed data package
        delegate = self.delegate
        if delegate is not None:
            remote = self.remote_address
            local = self.local_address
            conn = self.connection
            delegate.gate_received(ship=income, source=remote, destination=local, connection=conn)

    @abstractmethod
    def _get_arrival(self, data: bytes) -> Optional[Arrival]:
        """
        Get income ship from received data

        :param data: received data
        :return income ship carrying data package/fragment
        """
        raise NotImplemented

    @abstractmethod
    def _check_arrival(self, ship: Arrival) -> Optional[Arrival]:
        """
        Check income ship for responding

        :param ship: income ship carrying data package/fragment/response
        :return income ship carrying completed data package
        """
        raise NotImplemented

    # protected
    def _check_response(self, ship: Arrival) -> Optional[Departure]:
        """ Check and remove linked departure ship with same SN (and page index for fragment) """
        # check response for linked departure ship (same SN)
        linked = self.__dock.check_response(ship=ship)
        if linked is None:
            # linked departure task not found, or not finished yet
            return None
        # all fragments responded, task finished
        delegate = self.delegate
        if delegate is not None:
            remote = self.remote_address
            local = self.local_address
            conn = self.connection
            delegate.gate_sent(ship=linked, source=local, destination=remote, connection=conn)
        return linked

    # protected
    def _assemble_arrival(self, ship: Arrival) -> Optional[Arrival]:
        """ Check received ship for completed package """
        return self.__dock.assemble_arrival(ship=ship)

    # protected
    def _next_departure(self, now: int) -> Optional[Departure]:
        """ Get outgo ship from waiting queue """
        # this will be remove from the queue,
        # if needs retry, the caller should append it back
        return self.__dock.next_departure(now=now)

    # Override
    def append_departure(self, ship: Departure) -> bool:
        """ Append outgo Ship to the waiting queue """
        return self.__dock.append_departure(ship=ship)

    # Override
    def purge(self):
        """ Clear expired tasks """
        self.__dock.purge()

    # Override
    def close(self):
        hub = self.hub
        if hub is not None:
            remote = self.remote_address
            local = self.local_address
            hub.disconnect(remote=remote, local=local)

# -*- 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 abstractmethod
from typing import Optional, List

from .types import AddressPairObject
from .net import Connection, Hub
from .port import Arrival, Departure
from .port import Docker, Gate, GateDelegate

from .dock import Dock, LockedDock


class StarDocker(AddressPairObject, Docker):
    """
        Star Docker
        ~~~~~~~~~~~

        @abstract properties:
            connection()
            delegate()

        @abstract methods:
            - pack(payload, priority)
            - heartbeat()
            - _get_arrival(data)
            - _check_arrival(ship)
    """

    def __init__(self, remote: tuple, local: Optional[tuple], gate: Gate):
        super().__init__(remote=remote, local=local)
        self.__gate = weakref.ref(gate)
        self.__dock = self._create_dock()
        # remaining data to be sent
        self.__last_outgo: Optional[Departure] = None
        self.__last_fragments: List[bytes] = []

    # noinspection PyMethodMayBeStatic
    def _create_dock(self) -> Dock:
        """ Override for user-customized dock """
        return LockedDock()

    @property  # protected
    def gate(self) -> Gate:
        return self.__gate()

    @property  # protected
    def hub(self) -> Optional[Hub]:
        raise NotImplemented

    @property  # Override
    def remote_address(self) -> tuple:  # (str, int)
        return self._remote

    @property  # Override
    def local_address(self) -> Optional[tuple]:  # (str, int)
        return self._local

    @property  # Override
    def connection(self) -> Optional[Connection]:
        gate = self.gate
        if gate is not None:
            return gate.get_connection(remote=self._remote, local=self._local)

    @property  # Override
    def delegate(self) -> Optional[GateDelegate]:
        gate = self.gate
        if gate is not None:
            return gate.delegate

    # Override
    def process(self) -> bool:
        # 1. get connection which is ready for sending data
        conn = self.connection
        if conn is None or not conn.alive:
            # connection not ready now
            return False
        # 2. get data to be sent
        if len(self.__last_fragments) > 0:
            # get remaining fragments from last outgo task
            outgo = self.__last_outgo
            fragments = self.__last_fragments
            self.__last_outgo = None
            self.__last_fragments = []
        else:
            # get next outgo task
            now = int(time.time())
            outgo = self._next_departure(now=now)
            if outgo is None:
                # nothing to do now, return False to let the thread have a rest
                return False
            elif outgo.is_failed(now=now):
                # task timeout, return True to process next one
                error = TimeoutError('Request timeout')
                self.__on_error(error=error, ship=outgo, connection=conn)
                return True
            else:
                # get fragments from outgo task
                fragments = outgo.fragments
                if len(fragments) == 0:
                    # all fragments of this task have been sent already
                    # return True to process next one
                    return True
        # 3. process fragments of outgo task
        index = 0
        sent = 0
        try:
            remote_address = self.remote_address
            for fra in fragments:
                sent = conn.send(data=fra, target=remote_address)
                if sent < len(fra):
                    # buffer overflow?
                    break
                else:
                    index += 1
            fra_cnt = len(fragments)
            if index < fra_cnt:
                # task failed
                error = ConnectionError('only %d/%d fragments sent' % (index, fra_cnt))
            else:
                # task done
                return True
        except Exception as e:
            # socket error, callback
            error = e
        # remove sent fragments
        while index > 0:
            fragments.pop(0)
            index -= 1
        # remove partially sent data of next fragment
        if sent > 0:
            last = fragments.pop(0)
            fragments.insert(0, last[sent:])
        # store remaining data
        self.__last_outgo = outgo
        self.__last_fragments = fragments
        # callback for error
        self.__on_error(error=error, ship=outgo, connection=conn)

    def __on_error(self, error, ship: Departure, connection: Connection):
        # callback for error
        delegate = self.delegate
        if delegate is not None:
            remote = self.remote_address
            local = self.local_address
            delegate.gate_error(error=error, ship=ship, source=local, destination=remote, connection=connection)

    # Override
    def process_received(self, data: bytes):
        # 1. get income ship from received data
        income = self._get_arrival(data=data)
        if income is None:
            return None
        # 2. check income ship for response
        income = self._check_arrival(ship=income)
        if income is None:
            return None
        # 3. callback for processing income ship with completed data package
        delegate = self.delegate
        if delegate is not None:
            remote = self.remote_address
            local = self.local_address
            conn = self.connection
            delegate.gate_received(ship=income, source=remote, destination=local, connection=conn)

    @abstractmethod
    def _get_arrival(self, data: bytes) -> Optional[Arrival]:
        """
        Get income ship from received data

        :param data: received data
        :return income ship carrying data package/fragment
        """
        raise NotImplemented

    @abstractmethod
    def _check_arrival(self, ship: Arrival) -> Optional[Arrival]:
        """
        Check income ship for responding

        :param ship: income ship carrying data package/fragment/response
        :return income ship carrying completed data package
        """
        raise NotImplemented

    # protected
    def _check_response(self, ship: Arrival) -> Optional[Departure]:
        """ Check and remove linked departure ship with same SN (and page index for fragment) """
        # check response for linked departure ship (same SN)
        linked = self.__dock.check_response(ship=ship)
        if linked is None:
            # linked departure task not found, or not finished yet
            return None
        # all fragments responded, task finished
        delegate = self.delegate
        if delegate is not None:
            remote = self.remote_address
            local = self.local_address
            conn = self.connection
            delegate.gate_sent(ship=linked, source=local, destination=remote, connection=conn)
        return linked

    # protected
    def _assemble_arrival(self, ship: Arrival) -> Optional[Arrival]:
        """ Check received ship for completed package """
        return self.__dock.assemble_arrival(ship=ship)

    # protected
    def _next_departure(self, now: int) -> Optional[Departure]:
        """ Get outgo ship from waiting queue """
        # this will be remove from the queue,
        # if needs retry, the caller should append it back
        return self.__dock.next_departure(now=now)

    # Override
    def append_departure(self, ship: Departure) -> bool:
        """ Append outgo Ship to the waiting queue """
        return self.__dock.append_departure(ship=ship)

    # Override
    def purge(self):
        """ Clear expired tasks """
        self.__dock.purge()

    # Override
    def close(self):
        hub = self.hub
        if hub is not None:
            remote = self.remote_address
            local = self.local_address
            hub.disconnect(remote=remote, local=local)

# -*- 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. # ============================================================================== Star Docker         ~~~~~~~~~~~          @abstract properties:             connection()             delegate()          @abstract methods:             - pack(payload, priority)             - heartbeat()             - _get_arrival(data)             - _check_arrival(ship) # remaining data to be sent # noinspection PyMethodMayBeStatic Override for user-customized dock # protected # protected # Override # (str, int) # Override # (str, int) # Override # Override # Override # 1. get connection which is ready for sending data # connection not ready now # 2. get data to be sent # get remaining fragments from last outgo task # get next outgo task # nothing to do now, return False to let the thread have a rest # task timeout, return True to process next one # get fragments from outgo task # all fragments of this task have been sent already # return True to process next one # 3. process fragments of outgo task # buffer overflow? # task failed # task done # socket error, callback # remove sent fragments # remove partially sent data of next fragment # store remaining data # callback for error # callback for error # Override # 1. get income ship from received data # 2. check income ship for response # 3. callback for processing income ship with completed data package Get income ship from received data          :param data: received data         :return income ship carrying data package/fragment Check income ship for responding          :param ship: income ship carrying data package/fragment/response         :return income ship carrying completed data package # protected Check and remove linked departure ship with same SN (and page index for fragment) # check response for linked departure ship (same SN) # linked departure task not found, or not finished yet # all fragments responded, task finished # protected Check received ship for completed package # protected Get outgo ship from waiting queue # this will be remove from the queue, # if needs retry, the caller should append it back # Override Append outgo Ship to the waiting queue # Override Clear expired tasks # Override