from typing import Any, Callable, List, Literal, Optional, Self, Sequence
from yarnover.exceptions import KnittingException, PatternException

EvenInt = int
UnevenInt = int

def equal_parts(number : int, parts : int) -> Sequence[int]:
    a, b = divmod(number, parts)
    result = [a]*parts
    for i in range(b):
        result[i] += 1
    return result

def divide(number : int, *ratios : float) -> Sequence[int]:
    result = [round(ratio*number) for ratio in ratios]
    remainder = number - sum(result)
    result[0] += remainder
    return result

def even(i : int) -> EvenInt:
    if i % 2:
        raise PatternException(f"Expecting an even number, got {i}")
    else:
        return EvenInt(i)

def uneven(i : int) -> UnevenInt:
    if not(i % 2):
        raise PatternException(f"Expecting an uneven number, got {i}")
    else:
        return UnevenInt(i)

def make_uneven(i : int, add_one : bool = True) -> UnevenInt:
    if i % 2 == 0:
        if add_one:
            return UnevenInt(i + 1)
        else:
            return UnevenInt(i - 1)
    return UnevenInt(i)

def make_even(i : int, add_one : bool = True) -> UnevenInt:
    if i % 2 == 1:
        if add_one:
            return UnevenInt(i + 1)
        else:
            return UnevenInt(i - 1)
    return UnevenInt(i)

def half(i):
    return even(i) // 2

class Predicate:
    def __init__(self, length : int):
        self.length : int = length
    
    def then(self, other : "Predicate"):
        chain = ChainedPredicate()
        chain.add_predicate(self)
        chain.add_predicate(other)
        return chain

    def items(self):
        for i in range(self.length):
            yield i, self(i)

    def values(self):
        for i in range(self.length):
            yield self(i)

    def true_count(self):
        result = 0
        for _, value in self.items():
            if value:
                result += 1
        return result

class ChainedPredicate(Predicate):
    """A composite predicate. Evaluates to True if any component predicate evaluates to True.
    """
    def __init__(self, *predicates : Predicate):
        super().__init__(0)
        self.predicates : List[Predicate] = list()
        for predicate in predicates:
            self.add_predicate(predicate)

    def __call__(self, i : int) -> bool:
        if i >= self.length:
            return False
        offset = 0
        for predicate in self.predicates:
            if i-offset < predicate.length:
                return predicate(i-offset)
            else:
                offset += predicate.length
        else:
            raise PatternException("Out of bounds")  

    def add_predicate(self, predicate : Predicate):
        self.length += predicate.length
        self.predicates.append(predicate)

    def then(self, predicate : Predicate):
        self.add_predicate(predicate)
        return self

class Skip(Predicate):
    def __init__(self, length):
        """Selects no items out of `length` items"""
        super().__init__(length)

    def __call__(self, i : int) -> bool:
        return False
        
    def __repr__(self) -> str:
        return f"<Skip: Skip {self.length} items>"

class Nth(Predicate):
    def __init__(self, n):
        "Select the `n`th item."
        super().__init__(n)
        self.n = n

    def __call__(self, i : int) -> bool:
        return i == self.n

class First(Nth):
    def __init__(self):
        "Selects the first item (equivalent to `Nth(0)`"
        super().__init__(0)

class Last(Predicate):
    def __init__(self, count : int, length : int):
        "Selects the last `count` items"
        super().__init__(length)
        self.count = count

    def __call__(self, i : int) -> Any:
        if i >= self.length:
            return False
        return self.length - i <= self.count

class Every(Predicate):
    def __init__(
            self,
            distance : int,
            length : int,
            position : int = 0
            ):
        """Selects every `n`'th item from `length` items.

        :param space: The space between selected items - 2 means that every other item is selected
        :param length: The total rumber of items
        :param position: The position of the selected item in each block of items. 0 is the first items, -1 is the last
        """
        super().__init__(length)
        self.distance = distance
        self.position = position if position >= 0 else distance + position

    def __repr__(self) -> str:
        return f"<Every: Every {self.distance} item over {self.length} items>"

    def __call__(self, i : int) -> bool:
        if i >= self.length:
            return False
        remainder = i % self.distance
        return remainder == self.position

class Over(ChainedPredicate):
    """Evenly spaces `count` events over `length` items
    """
    def __init__(self,
                 count : int,
                 length : int,
                 position : int = -1,
                 long_spaces_first : bool = True):
        super().__init__()
        assert count <= length
        self.count = count
        short_space, long_space_count = divmod(length, count)
        short_space_count = count - long_space_count
        long_space = short_space + 1
        if long_space_count and long_spaces_first:
            self.add_predicate(Every(long_space, long_space_count*long_space, position))
        self.add_predicate(Every(short_space, short_space_count*short_space, position))
        if long_space_count and not long_spaces_first:
            self.add_predicate(Every(long_space, long_space_count*long_space, position))

    def __repr__(self) -> str:
        return f"<Over: {self.count} items out of {self.length}>"

def select():
    return ChainedPredicate()

def chain(*predicates : Predicate):
    return ChainedPredicate(*predicates)