import itertools
from collections import defaultdict
from collections.abc import Iterable, Mapping, Collection

type CardSort[T] = Collection[Iterable[T]]


def _raw_similarity(sorts):
    result = defaultdict(lambda: defaultdict(int))
    count = 0
    for sort in sorts:
        count += 1
        for group in sort:
            for card in group:
                result[card][card] += 1
            for c1, c2 in itertools.combinations(group, 2):
                result[c1][c2] += 1
                result[c2][c1] += 1
    return count, result


def similarity_mapping[T](
        sorts: Iterable[CardSort[T]],
        *,
        scale: bool = False,
) -> Mapping[T, Mapping[T, float]]:
    count, result = _raw_similarity(sorts)
    cards = list(result)
    if scale:
        return {
            c1: {c2: result[c1][c2] / count for c2 in cards}
            for c1 in cards
        }
    else:
        return {
            c1: {c2: result[c1][c2] for c2 in cards}
            for c1 in cards
        }


def similarity_matrix[T](
        sorts: Iterable[CardSort[T]],
        *,
        scale: bool = False,
) -> list[list[None | T | float]]:
    """
    Computes the similarity matrix of the given sorts as a list of lists with
    the first row and column containing card labels. If `scale` is set to True,
    the similarity counts are scaled to be the proportion (between 0 and 1) of
    times any two cards were placed together.

    :param sorts: An iterable of card sorts
    :param scale: Whether similarity counts should be scaled to a proportion
        between 0 and 1
    :return:
    """
    count, result = _raw_similarity(sorts)
    cards = list(result)
    if scale:
        scaled = (
            [card1, *(result[card1][card2] / count for card2 in cards)]
            for card1 in cards
        )
        return [[None, *cards], *scaled]
    else:
        values = (
            [card1, *(result[card1][card2] for card2 in cards)]
            for card1 in cards
        )
        return [[None, *cards], *values]


def dissimilarity_mapping[T](
        sorts: Iterable[CardSort[T]],
        *,
        scale: bool = False,
) -> Mapping[T, Mapping[T, float]]:
    count, result = _raw_similarity(sorts)
    cards = list(result)
    if scale:
        return {
            c1: {c2: 1 - (result[c1][c2] / count) for c2 in cards}
            for c1 in cards
        }
    else:
        return {
            c1: {c2: count - result[c1][c2] for c2 in cards}
            for c1 in cards
        }


def dissimilarity_matrix[T](
        sorts: Iterable[CardSort[T]],
        *,
        scale: bool = False,
) -> list[list[None | T | float]]:
    count, result = _raw_similarity(sorts)
    cards = list(result)
    if scale:
        scaled = (
            [card1, *(1 - (result[card1][card2] / count) for card2 in cards)]
            for card1 in cards
        )
        return [[None, *cards], *scaled]
    else:
        values = (
            [card1, *(count - result[card1][card2] for card2 in cards)]
            for card1 in cards
        )
        return [[None, *cards], *values]
