"""
Functions for evaluating bridge hands using a variety of different metrics.
"""

from __future__ import annotations

__all__ = [
    "standard_hcp_scale",
    "bergen_hcp_scale",
    "hcp",
    "shortage_nofit_dist_scale",
    "shortage_fit_dist_scale",
    "length_dist_scale",
    "mixed_fit_dist_scale",
    "mixed_nofit_dist_scale",
    "dist_points",
    "total_points",
    "top_honours",
    "losers",
    "cccc",
    "quality",
    "controls",
    "rule_of_n",
    "exact_shape",
    "shape",
    "major_shape",
    "minor_shape",
    "is_balanced",
    "is_semibalanced",
    "is_minor_semibalanced",
    "is_single_suited",
    "is_two_suited",
    "is_three_suited",
]

from collections.abc import Iterable
from typing import Optional, Union

from endplay.types import AlternateRank, Card, Denom, Hand, Rank, SuitHolding

# Points for each rank in order [A, K, Q, J, ...]
standard_hcp_scale: list[float] = [4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
"""Classic HCP scale with A=4, K=3, Q=2, J=1"""
bergen_hcp_scale: list[float] = [4.5, 3, 1.5, 0.75, 0.25, 0, 0, 0, 0, 0, 0, 0, 0]
"""Bergen HCP scale with A=4.5, K=3, Q=1.5, J=0.75, T=0.25"""


def hcp(
    obj: Union[Iterable[Union[Card, Rank, AlternateRank]], Card, Rank, AlternateRank],
    scale: list[float] = standard_hcp_scale,
) -> float:
    """
    Return the high card points of a hand or suit holding using a given scale. Two
    scales are predefined: `standard_hcp_scale` which uses the traditional 4321
    point scale, and `bergen_hcp_scale` which uses the Bergen scale.

    :param obj: A (possibly iterable of) card or rank type to evaluate
    :param scale: A list of 13 numbers which assign points to each rank in descending
            order, e.g. [4,3,2,1,0,...] is used for the standard HCP scale where an ace is
            worth 4 points, a king 3 etc...
    """
    if len(scale) < 13:
        scale += [0] * (13 - len(scale))
    if isinstance(obj, Card):
        return scale[14 - obj.rank.to_alternate()]
    elif isinstance(obj, Rank):
        return scale[14 - obj.to_alternate()]
    elif isinstance(obj, AlternateRank):
        return scale[14 - obj]
    else:
        return sum(hcp(card, scale) for card in obj)


# Points for each suit length in order [0/void, 1/singleton, 2/doubleton, 3, ...]
shortage_nofit_dist_scale: list[float] = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
"""Distribution point scale with void=3, singleton=2, doubleton=1"""
shortage_fit_dist_scale: list[float] = [5, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
"""Distribution point scale when a trump fit is found with void=5, singleton=3, doubleton=1"""
length_dist_scale: list[float] = [0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""Distribution point scale which awards a point for each extra card in a suit over 4"""
mixed_nofit_dist_scale: list[float] = [3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""Combined scale using `shortage_nofit_dist_scale` and `length_dist_scale`"""
mixed_fit_dist_scale: list[float] = [5, 3, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""Combined scale using `shortage_fit_dist_scale` and `length_dist_scale`"""


def dist_points(
    obj: Union[Hand, SuitHolding],
    scale: list[float] = shortage_nofit_dist_scale,
    exclude: Iterable[Denom] = [],
) -> float:
    """
    Return the extra points for distribution for a hand or suit holding using the given scale.
    Five scales are predefined:

    * `shortage_nofit_dist_scale`: Standard evaluation of shortage points if no trump fit is agreed,
      void=3, singleton=2, doubleton=1

    * `shortage_fit_dist_scale`: Standard evaluation of shortage points if trump fit is agreed,
      void=5, singleton=3, doubleton=1

    * `length_dist_scale`: Standard evaluation of length points, one point for each extra card in a
      suit with more than four cards in it, i.e. 5 cards=1, 6 cards=2, etc...

    :param obj: Hand or suit holding to evaluate
    :param scale: A list of 14 numbers which assign points to each suit length in ascending order,
            e.g. [3,2,1,0,...] is used for the standard shortage points where void=3, singleton=2 etc...
    :param exclude: List of suits to be excluded from distribution calculations if a `Hand` object
            is passed, in order to e.g. exclude the trump suit from the calculation
    """
    if len(scale) < 14:
        scale += [0] * (14 - len(scale))
    if isinstance(obj, SuitHolding):
        return scale[len(obj)]
    return sum(
        dist_points(obj[suit], scale) if suit not in exclude else 0
        for suit in Denom.suits()
    )


def total_points(
    obj: Union[Hand, SuitHolding],
    hcp_scale: list[float] = standard_hcp_scale,
    dist_scale: list[float] = shortage_nofit_dist_scale,
    trump: Optional[Denom] = None,
    protect_honours: bool = False,
) -> float:
    """
    Return the sum of high card points and distribution points in a hand.

    :param obj: Hand or suit holding to evaluate
    :param hcp_scale: The HCP scale to use, as for the `hcp` function
    :param dist_scale: The distribution points scale, as for the `dist_scale` function
    :param trump: If set then distribution points for the given suit are excluded from
            the calculation
    :param protect_honours: If `True`, then honours must be protected for their HCP
            to be included, i.e. king singleton, queen doubleton etc do not contribute to
            total points
    """
    if isinstance(obj, SuitHolding):
        points = dist_points(obj, dist_scale, [trump] if trump is not None else [])
        if protect_honours:
            obj = obj.copy()
            ncards = len(obj)
            if ncards < 4:
                obj.remove(Rank.RJ)
            if ncards < 3:
                obj.remove(Rank.RQ)
            if ncards < 2:
                obj.remove(Rank.RK)
        return hcp(obj, hcp_scale) + points
    else:
        return sum(
            total_points(obj[suit], hcp_scale, dist_scale, trump, protect_honours)
            for suit in Denom.suits()
        )


def top_honours(
    hand: Union[Hand, SuitHolding], lowest_honour: Union[Rank, int] = Rank.RJ
) -> int:
    """
    Return the number of top honors in a suit

    :param suit: The suit holding to evaluate
    :param lowest_honour: The lowest rank to be treated as an honour, or an
            integer for how many top cards are honours
    """
    if not isinstance(lowest_honour, Rank):
        lowest_honour = AlternateRank(15 - lowest_honour).to_standard()
    if isinstance(hand, SuitHolding):
        return sum(1 for rank in hand if rank >= lowest_honour)
    else:
        return sum(1 for card in hand if card.rank >= lowest_honour)


def losers(hand: Union[Hand, SuitHolding]) -> int:
    """
    Returns the number of losers in a hand. The number of losers in each
    suit is calculated as: (a) 3 or more cards in a suit: 3 - 1 per AKQ
    (b) Doubleton: AK=0, Ax/Kx=1, else 2 (c) Singleton: A=0, else 1 (d) Void: 0
    """
    if isinstance(hand, SuitHolding):
        a, k, q = Rank.RA in hand, Rank.RK in hand, Rank.RQ in hand
        if len(hand) == 0:
            return 0
        if len(hand) == 1:
            return 1 - a
        if len(hand) == 2:
            return 2 - a - k
        return 3 - a - k - q
    else:
        return sum(losers(hand[suit]) for suit in Denom.suits())


def cccc(hand: Union[Hand, SuitHolding]) -> float:
    """
    Uses the Kaplan four cs algorithm from the October 1982 issue of Bridge world
    magazine. This implementation is based on the interpretation of the algorithm
    described on http://www.rpbridge.net/8j19.htm
    """
    if isinstance(hand, Hand):
        part = sum(cccc(hand[suit]) for suit in Denom.suits()) - 1
        if shape(hand) == [4, 3, 3, 3]:
            return part + 0.5
        return part
    l = len(hand)
    score = 0.0
    # 1-5: Count point values A=4,K=3,Q=2,J=1,T=0.5
    if Rank.RA in hand:
        score += 4
    if Rank.RK in hand:
        score += 3
    if Rank.RQ in hand:
        score += 2
    if Rank.RJ in hand:
        score += 1
    if Rank.RT in hand:
        score += 0.5
    # 6-7: if between 2-6 cards, count points for supported T and 9
    if l >= 2 and l <= 6:
        if (Rank.RT in hand) and ((Rank.RJ in hand) or top_honours(hand, 4) >= 2):
            score += 0.5
        if (Rank.R9 in hand) and (
            (Rank.R8 in hand or Rank.RT in hand) or top_honours(hand, 5) == 2
        ):
            score += 0.5
    # 8: if between 4-6 cards, count 9 without any touching cards and exactly three higher honours
    if l >= 4 and l <= 6:
        if (
            (Rank.R9 in hand)
            and not (Rank.R8 in hand and Rank.RT in hand)
            and top_honours(hand, 4) == 3
        ):
            score += 0.5
    # 9: 7+ cards missing queen or jack (or both)
    if l >= 7 and (Rank.RQ not in hand or Rank.RJ not in hand):
        score += 1
    # 10: 8+ cards missing queen
    if l >= 8 and Rank.RQ not in hand:
        score += 1
    # 11: 9+ cards missing queen and jack
    if l >= 9 and (Rank.RQ not in hand and Rank.RJ not in hand):
        score += 1
    # 12: Multiply current total by suit length, divide by 10, then continue
    score *= l / 10
    # 13: Add 3 for an ace
    if Rank.RA in hand:
        score += 3
    # 14-15: Add 2 for guarded king and 0.5 for king singleton
    if Rank.RK in hand:
        if l > 1:
            score += 2
        else:
            score += 0.5
    # 16-19: Add values for queen
    if Rank.RQ in hand:
        if l >= 3:
            # 16-17: 3+ cards, +1 if has higher honour else +0.75
            if Rank.RK in hand or Rank.RA in hand:
                score += 1
            else:
                score += 0.75
        elif l == 2:
            # 18-19: doubleton, +0.5 if has higher honour else +0.25
            if Rank.RK in hand or Rank.RA in hand:
                score += 0.5
            else:
                score += 0.25
    # 20-21: Jack with two higher honours=0.5, one higher honour=0.25
    if Rank.RJ in hand:
        hh = top_honours(hand, 3)
        if hh == 2:
            score += 0.5
        elif hh == 1:
            score += 0.25
    # 22-23: Ten with two higher honours=0.25, T9 with one higher honour=0.25
    if Rank.RT in hand:
        hh = top_honours(hand, 4)
        if hh == 2:
            score += 0.25
        if Rank.R9 in hand and hh == 1:
            score += 0.25
    # 24-26: void=3, singleton=2, doubleton=1
    if l == 0:
        score += 3
    elif l == 1:
        score += 2
    elif l == 2:
        score += 1
    return score


def quality(hand: SuitHolding) -> float:
    "Uses the quality algorithm (from the October 1982 issue of Bridge World magazine)"
    raise NotImplementedError


def controls(hand: Union[Iterable, Card, Rank]) -> int:
    "Return the number of controls in a sequence, using A=2 and K=1"
    if isinstance(hand, Rank):
        if hand == Rank.RA:
            return 2
        elif hand == Rank.RK:
            return 1
        else:
            return 0
    elif isinstance(hand, Card):
        return controls(hand.rank)
    else:
        return sum(controls(card) for card in hand)


def rule_of_n(hand: Hand) -> float:
    "Returns the hcp of the hand added to the length of the two longest suits"
    s = shape(hand)
    return hcp(hand) + s[0] + s[1]


def exact_shape(hand: Hand) -> list[int]:
    "Return the shape of a hand as a list starting from spades, e.g. (5, 2, 3, 3)"
    return [len(hand[suit]) for suit in Denom.suits()]


def shape(hand: Hand) -> list[int]:
    """
    Return the shape of a hand as a list from longest to shortest, e.g. a 3424 would
    return (4, 4, 3, 2)
    """
    return sorted(exact_shape(hand), reverse=True)


def major_shape(hand: Hand) -> list[int]:
    "Return the shape of a hand's major holding from longest to shortest"
    return sorted((len(hand[Denom.spades]), len(hand[Denom.hearts])), reverse=True)


def minor_shape(hand: Hand) -> list[int]:
    """
    Return the shape of a hand's minor holding from longest to shortest
    """
    return sorted((len(hand[Denom.diamonds]), len(hand[Denom.clubs])), reverse=True)


def is_balanced(hand: Hand) -> bool:
    """
    Returns True if the hand shape is 4333, 4432 or 5332
    """
    s = shape(hand)
    return s in ([4, 3, 3, 3], [4, 4, 3, 2], [5, 3, 3, 2])


def is_semibalanced(hand: Hand) -> bool:
    """
    Returns True if the hand shape is balanced or 5422
    """
    s = shape(hand)
    return is_balanced(hand) or s == [5, 4, 2, 2]


def is_minor_semibalanced(hand: Hand) -> bool:
    """
    Returns True if the hand shape is balanced or contains
    doubletons in both minors
    """
    return is_balanced(hand) or minor_shape(hand) == [2, 2]


def is_single_suited(
    hand: Hand, min_length: int = 6, no_side_suit: bool = False
) -> bool:
    """
    Returns True if the hand is singled suited

    :param min_length: The minimum number of cards to hold in the suit
    :param no_side_suit: If True, the hand cannot contain another 4 card suit
    """
    s = shape(hand)
    if no_side_suit:
        return s[0] >= min_length and s[1] < 4
    else:
        return s[0] >= min_length


def is_two_suited(hand: Hand, strict: bool = False) -> bool:
    """
    Returns True if the hand contains at least 10 cards in two suits.

    :param strict: Hand must be at least 5-5
    """
    s = shape(hand)
    if strict:
        return s[0] + s[1] >= 10 and s[1] >= 5
    else:
        return s[0] + s[1] >= 10


def is_three_suited(hand: Hand, strict: bool = False) -> bool:
    """
    Returns True if the hand has three suits with at least four cards in them.

    :param strict: Only return true if the hand is 4441
    """
    s = shape(hand)
    if strict:
        return s == [4, 4, 4, 1]
    else:
        return s[0] >= 4 and s[1] >= 4 and s[2] >= 4
