from __future__ import annotations

from dataclasses import dataclass
from fractions import Fraction
from typing import Sequence, cast

from typing_extensions import Protocol


class EdgeProtocol(Protocol):
    """Any object that defines an edge (such as Layout)."""

    # Size of edge in cells, or None for no fixed size
    size: int | None
    # Portion of flexible space to use if size is None
    fraction: int
    # Minimum size for edge, in cells
    min_size: int


@dataclass
class Edge:
    size: int | None = None
    fraction: int | None = 1
    min_size: int = 1


def layout_resolve(total: int, edges: Sequence[EdgeProtocol]) -> list[int]:
    """Divide total space to satisfy size, fraction, and min_size, constraints.

    The returned list of integers should add up to total in most cases, unless it is
    impossible to satisfy all the constraints. For instance, if there are two edges
    with a minimum size of 20 each and `total` is 30 then the returned list will be
    greater than total. In practice, this would mean that a Layout object would
    clip the rows that would overflow the screen height.

    Args:
        total: Total number of characters.
        edges: Edges within total space.

    Returns:
        Number of characters for each edge.
    """
    # Size of edge or None for yet to be determined
    sizes = [(edge.size or None) for edge in edges]

    if None not in sizes:
        # No flexible edges
        return cast("list[int]", sizes)

    # Get flexible edges and index to map these back on to sizes list
    flexible_edges = [
        (index, edge)
        for index, (size, edge) in enumerate(zip(sizes, edges))
        if size is None
    ]
    # Remaining space in total
    remaining = total - sum([size or 0 for size in sizes])
    if remaining <= 0:
        # No room for flexible edges
        return [
            ((edge.min_size or 1) if size is None else size)
            for size, edge in zip(sizes, edges)
        ]

    # Get the total fraction value for all flexible edges
    total_flexible = sum([(edge.fraction or 1) for _, edge in flexible_edges])
    while flexible_edges:
        # Calculate number of characters in a ratio portion
        portion = Fraction(remaining, total_flexible)

        # If any edges will be less than their minimum, replace size with the minimum
        for flexible_index, (index, edge) in enumerate(flexible_edges):
            if portion * edge.fraction < edge.min_size:
                # This flexible edge will be smaller than its minimum size
                # We need to fix the size and redistribute the outstanding space
                sizes[index] = edge.min_size
                remaining -= edge.min_size
                total_flexible -= edge.fraction or 1
                del flexible_edges[flexible_index]
                # New fixed size will invalidate calculations, so we need to repeat the process
                break
        else:
            # Distribute flexible space and compensate for rounding error
            # Since edge sizes can only be integers we need to add the remainder
            # to the following line
            remainder = Fraction(0)
            for index, edge in flexible_edges:
                sizes[index], remainder = divmod(portion * edge.fraction + remainder, 1)
            break

    # Sizes now contains integers only
    return cast("list[int]", sizes)
