# # ⚠ Warning
#
# 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.
#
# [🥭 Entropy Markets](https://entropy.trade/) support is available at:
#   [Docs](https://docs.entropy.trade/)
#   [Discord](https://discord.gg/67jySBhxrg)
#   [Twitter](https://twitter.com/entropymarkets)
#   [Github](https://github.com/blockworks-foundation)
#   [Email](mailto:hello@blockworks.foundation)

import abc
import entropy
import typing

from .element import Element
from ...modelstate import ModelState


# # 🥭 PairwiseElement class
#
# Some `Element`s want to process `Order`s in pairs or levels. Typically they'll have a BUY and a
# SELL at one spread, another BUY and SELL at a second spread, another BUY and SELL at a third
# spread, and so on.
#
# The 'desired orders' that come in don't fit that pattern though. It's just a list of `Order`s.
# The `Order`s may have a pair-wise structure, but it's not imposed upon them.
#
# If the `Order`s _do_ have such a pair-wise structure, some `Element`s can take advantage of that.
#
# (The assumption is that if your `Order`s _don't_ have a pair-wise structure - a matching BUY for
# each SELL - then you don't expect to successfully use `Element`s that depend upon such a
# structure.)
#
# The `PairwiseElement` handles converting from an unstructured list of `Order`s into a pair-wise
# structure, and then calls the derived class's `process_order_pair()` method to process a pair.
#
class PairwiseElement(Element, metaclass=abc.ABCMeta):
    def __init__(self) -> None:
        super().__init__()

    @abc.abstractmethod
    def process_order_pair(
        self,
        context: entropy.Context,
        model_state: ModelState,
        index: int,
        buy: typing.Optional[entropy.Order],
        sell: typing.Optional[entropy.Order],
    ) -> typing.Tuple[typing.Optional[entropy.Order], typing.Optional[entropy.Order]]:
        raise NotImplementedError(
            "PairwiseElement.process_order_pair() is not implemented on the base type."
        )

    # If multiple levels are specified, we want to process them in order.
    #
    # But 'in order' is complicated. The way it will be expected to work is:
    # * First BUY and first SELL are treated as a pair
    # * Second BUY and second SELL are treated as a pair
    # * Third BUY and third SELL are treated as a pair
    # * etc.
    # But (another but!) 'first' means closest to the top of the book to people, not necessarily
    # first in the incoming order list.
    #
    # We want to meet that expected approach, so we'll:
    # * Split the list into BUYs and SELLs
    # * Sort the two lists so closest to top-of-book is at index 0
    # * Call process_order_pair() for each paired BUY and SELL, with the index parameter being
    #   the index into the BUY and SELL lists.
    def process(
        self,
        context: entropy.Context,
        model_state: ModelState,
        orders: typing.Sequence[entropy.Order],
    ) -> typing.Sequence[entropy.Order]:
        buys: typing.List[entropy.Order] = list(
            [order for order in orders if order.side == entropy.Side.BUY]
        )
        buys.sort(key=lambda order: order.price, reverse=True)
        sells: typing.List[entropy.Order] = list(
            [order for order in orders if order.side == entropy.Side.SELL]
        )
        sells.sort(key=lambda order: order.price)

        pair_count: int = max(len(buys), len(sells))
        new_orders: typing.List[entropy.Order] = []
        for index in range(pair_count):
            old_buy: typing.Optional[entropy.Order] = (
                buys[index] if index < len(buys) else None
            )
            old_sell: typing.Optional[entropy.Order] = (
                sells[index] if index < len(sells) else None
            )

            (new_buy, new_sell) = self.process_order_pair(
                context, model_state, index, old_buy, old_sell
            )
            if new_buy is not None:
                new_orders += [new_buy]

            if new_sell is not None:
                new_orders += [new_sell]

        return new_orders

    def __str__(self) -> str:
        return "« PairwiseElement »"
