from collections.abc import Sequence, Iterable, Mapping
from typing import Any
import inspect
from abc import ABC, abstractmethod
import weakref

from PIL import ImageColor
from colour import Color
PASSED: str = "#1AFE49"
FAILED: str = "#FF124F"

def hex_to_rgba(color: str) -> tuple:
    return tuple([e/255 for e in ImageColor.getcolor(color, "RGBA")])

def gradient(color1: str, color2: str, num: int) -> list[tuple]:
    c1, c2 = Color(color1), Color(color2)
    colors = list(c2.range_to(c1, num))
    colors = [hex_to_rgba(str(c)) for c in colors]
    return colors

class BaseGrading(Mapping, ABC):
    __instances: list[Mapping] = list()
    
    def __init__(self, schema: dict[str, int | float], name=None, alt_name=None):
        all_str = all(isinstance(k, str) for k in schema.keys())
        assert all_str or all(isinstance(k, int) for k in schema.keys()), "Keys must be all of type (str, int)"
        assert all(isinstance(v, float) for v in schema.values()), "All values must be floats in range(0,1)"
        assert all(v <= 1 and v >= 0 for v in schema.values()), "All values must be floats in range(0,1)"
        if all_str:
            self.schema = dict()
            for k, v in schema.items():
                self.schema[k.title()] = v 
        else:
            self.schema = schema

        self.__class__.__instances.append(weakref.proxy(self))
        self.name = name
        self.alt_name = alt_name

    def __getitem__(self, index):
        if index >= len(self):
            raise IndexError
        return self.schema[index]

    def __len__(self) -> int:
        return len(self.schema) 

    def __contains__(self, item: int | str | float) -> bool:
        if isinstance(item, (int, str)):
            if isinstance(item, str):
                item = item.title()
            return item in self.schema
        if isinstance(item, float):
            return item <= 1 and item >= 0
        return False

    def __iter__(self) -> Iterable:
        yield from self.schema

    def __reversed__(self) -> Iterable:
        yield from reversed(self.schema)

    def __str__(self) -> str:
        return self.name

    def __repr__(self) -> str:
        return f"<{self.name}: ({str(self.schema)})>"

    def __eq__(self, other) -> bool:
        if other == self:
            return True
        if isinstance(other, BaseEval):
            return self.schema == other.schema
        return NotImplemented 

    def __ne__(self, other) -> bool:
        return not self.__eq__(other)

    def get(self, key: int | str) -> float | None:
        if isinstance(key, str):
            key = key.title()
        if key in self:
            return self.schema[key]
        return None

    def keys(self) -> tuple:
        return list(self.schema.keys())

    def values(self) -> tuple:
        return list(self.schema.values())
    
    def items(self) -> list[tuple]:
        return list(self.schema.items())
    
    @abstractmethod
    def has_passed(self, value: int | float, max: int | float) -> bool:
        pass

    @abstractmethod
    def get_grade(self, value: int | float, max: int | float) -> str | int:
        pass

    @abstractmethod
    def get_grade_color(self, value: int | float, max: int | float) -> tuple:
        pass
    
    @classmethod
    def get_instances(cls):
        yield from cls.__instances

    @classmethod
    def get_instance(cls, name: str):
        for instance in cls.__instances:
            if instance.alt_name == name:
                return instance

get_gradings = lambda: BaseGrading.get_instances()
get_grader = lambda name: BaseGrading.get_instance(name)

class StdPercentRule(BaseGrading):
    def has_passed(self, value: int | float, max: int | float) -> bool:
        return value >= max * self.schema["Passed"]

    def get_grade(self, value: int | float, max: int | float) -> str:
        return "Passed" if self.has_passed(value, max) else "Not Passed"

    def get_grade_color(self, value: int | float, max: int | float) -> tuple:
        if self.has_passed(value, max):
            return hex_to_rgba(PASSED)
        return hex_to_rgba(FAILED)

class StdGermanGrading(BaseGrading):
    def has_passed(self, value: int | float, max: int | float) -> bool:
        return value/max >= 0.45 

    def search_grade(self, value: float) -> int:
        if value <= 0:
            return min(self.schema.keys())

        searched = max(self.schema.keys())
        found = False
        while not found:
            if self.schema[searched] <= value:
                found = True
            else:
                searched -= 1
        return searched

    def get_grade(self, value: int | float, max: int | float) -> int:
        return self.search_grade(value/max)

    def get_grade_color(self, value: float, max: int | float) -> tuple:
        grade = self.get_grade(value, max)
        colors = gradient(PASSED, FAILED, len(self.schema)) 
        return colors[grade]

# Definitions
Std30PercentRule = StdPercentRule({
    "pAssed": 0.3,
    "Failed": 0.0
}, "Std30PercentRule", "30%")

Std50PercentRule = StdPercentRule({
    "Passed": 0.5,
    "Failed": 0.0
}, "Std50PercentRule", "50%")

StdGermanGradingMiddleSchool = StdGermanGrading({
    1: 0.96,
    2: 0.80,
    3: 0.60,
    4: 0.45,
    5: 0.16,
    6: 0.00
}, "StdGermanGradingMiddleSchool", "Mittelstufe")

StdGermanGradingHighSchool = StdGermanGrading({
    15: 0.95,
    14: 0.90,
    13: 0.85,
    12: 0.80,
    11: 0.75,
    10: 0.70,
    9: 0.65,
    8: 0.60,
    7: 0.55,
    6: 0.50,
    5: 0.45,
    4: 0.40,
    3: 0.33,
    2: 0.27,
    1: 0.20,
    0: 0.00
}, "StdGermanGradingHighSchool", "Oberstufe")


#print(StdGermanGradingHighSchool.get_grade(0.0, 24))

