from numba import njit
import numpy as np
from typing import Generator, Union
from src.shared.typings import CreatureCategory, CreatureCategoryOrUnknown, GrayImage
from src.utils.core import hashit, locate
from .config import creaturesNamesImagesHashes, currentCreaturesNames, currentCreaturesNamesImages, currentCreaturesNamesImagesHashes, images
from .extractors import getCreaturesNamesImages, getCurrentCreaturesNamesImages
from .typings import CreatureList, Creature


# PERF: [0.13737060000000056, 4.999999987376214e-07]
@njit(cache=True, fastmath=True)
def getBeingAttackedCreatureCategory(creatures: CreatureList) -> Union[CreatureCategory, None]:
    for creature in creatures:
        if creature['isBeingAttacked']:
            return creature['name']
    return None


# PERF: [1.3400000000274304e-05, 2.9000000001389026e-06]
@njit(cache=True, fastmath=True)
def getBeingAttackedCreatures(content: GrayImage, filledSlotsCount: int) -> Generator[bool, None, None]:
    alreadyCalculatedBeingAttackedCreature = False
    for creatureIndex in range(filledSlotsCount):
        contentIndex = creatureIndex * 22
        if alreadyCalculatedBeingAttackedCreature:
            yield False
        else:
            # detecting through corner pixels
            isBeingAttacked = (content[creatureIndex * 22, 0] == 76 or content[contentIndex, 0] == 166) and (content[contentIndex, 19] == 76 or content[contentIndex, 19] == 166) and (
                content[contentIndex + 19, 0] == 76 or content[contentIndex + 19, 0] == 166) and (content[contentIndex + 19, 19] == 76 or content[contentIndex + 19, 19] == 166)
            yield isBeingAttacked
            if isBeingAttacked:
                alreadyCalculatedBeingAttackedCreature = True


# PERF: [0.00017040000000001498, 7.330000000038694e-05]
def getCreatures(content: GrayImage) -> CreatureList:
    if content is not None:
        filledSlotsCount = getFilledSlotsCount(content)
        if filledSlotsCount == 0:
            return np.array([], dtype=Creature)
        beingAttackedCreatures = [
            beingAttackedCreature for beingAttackedCreature in getBeingAttackedCreatures(content, filledSlotsCount)]
        creaturesNames = [creatureName for creatureName in getCreaturesNames(
            content, filledSlotsCount)]
        creatures = np.array([(creatureName, beingAttackedCreatures[slotIndex])
                        for slotIndex, creatureName in enumerate(creaturesNames)], dtype=Creature)
        creaturesAfterCheck = checkDust(content, creatures)
        return creaturesAfterCheck
    else:
        return []


# PERF: [0.019119499999998624, 4.020000000082291e-05]
def getCreaturesNames(content: GrayImage, filledSlotsCount: int) -> Generator[CreatureCategoryOrUnknown, None, None]:
    legacyImages = getCreaturesNamesImages(content, filledSlotsCount)
    currentImages = getCurrentCreaturesNamesImages(content, filledSlotsCount)
    for creatureIndex, (legacyImage, currentImage) in enumerate(zip(legacyImages, currentImages)):
        creatureName = creaturesNamesImagesHashes.get(hashit(legacyImage))
        if creatureName is None:
            creatureName = currentCreaturesNamesImagesHashes.get(hashit(currentImage))
        if creatureName is None:
            distances = np.count_nonzero(
                currentCreaturesNamesImages != currentImage, axis=1
            )
            closestIndex = int(np.argmin(distances))
            creatureName = (
                currentCreaturesNames[closestIndex]
                if distances[closestIndex] <= 1 else 'Unknown'
            )
        if creatureName == 'Unknown':
            bestDistance = None
            bestName = 'Unknown'
            for rowOffset in range(7, 11):
                y = (creatureIndex * 22) + rowOffset
                if y >= content.shape[0]:
                    continue
                raw = content[y, 23:138]
                if raw.shape[0] != 115:
                    continue
                candidate = np.where(raw >= 160, 192, 0).astype(np.uint8)
                distances = np.count_nonzero(
                    currentCreaturesNamesImages != candidate, axis=1
                )
                closestIndex = int(np.argmin(distances))
                distance = int(distances[closestIndex])
                if bestDistance is None or distance < bestDistance:
                    bestDistance = distance
                    bestName = currentCreaturesNames[closestIndex]
            if bestDistance is not None and bestDistance <= 3:
                creatureName = bestName
        yield creatureName


# PERF: [0.5794668999999999, 3.9999999934536845e-07]
@njit(cache=True, fastmath=True)
def getFilledSlotsCount(content: GrayImage) -> int:
    filledSlotsCount = 0
    for slotIndex in range(len(content) // 22):
        y = 22 * slotIndex
        if content[:, 23][y + 11] == 192 or content[:, 23][y + 11] == 247:
            filledSlotsCount += 1
        elif content[:, 23][y + 10] == 192 or content[:, 23][y + 10] == 247:
            filledSlotsCount += 1
        elif content[:, 23][y + 4] == 192 or content[:, 23][y + 4] == 247:
            filledSlotsCount += 1
        elif content[:, 23][y + 5] == 192 or content[:, 23][y + 5] == 247:
            filledSlotsCount += 1
        else:
            break
    return filledSlotsCount


def getCreaturesHpPercentages(content: GrayImage, filledSlotsCount: int):
    """HP% je Battlelist-Zeile aus dem HP-Balken.

    Geometrie (aus echten Battlelist-Crops vermessen): Balken liegt auf
    Zeile `index * 22 + 17`, Spalten 22-151 (130 px), Fuellfarben sind
    gruen/gelb/rot (max. Kanal > 100), Hintergrund schwarz/grau.
    ponytail: Bei Balken, die nur die Farbe statt der Laenge aendern,
    wuerde immer 100% gelesen (Upgrade-Trigger: Fixture mit verletzter
    Kreatur zeigt anderes Verhalten).
    """
    hps = []
    for index in range(filledSlotsCount):
        y = (index * 22) + 17
        if y >= content.shape[0]:
            hps.append(None)
            continue
        bar = content[y, 22:152]
        if bar.shape[0] != 130:
            hps.append(None)
            continue
        filled = int(np.count_nonzero(bar > 100))
        hps.append(round(filled / 130 * 100))
    return hps


# PERF: [7.5999999999964984e-06, 7.999999986907369e-07]
def hasSkull(content: GrayImage, creatures: CreatureList) -> bool:
    for creatureIndex, creature in enumerate(creatures):
        if creature['name'] != 'Unknown':
            continue
        y = (creatureIndex * 22)
        creatureIconsImage = content[y + 2:y + 13, -38:-2]
        if locate(creatureIconsImage, images['skulls']['black']):
            return True
        if locate(creatureIconsImage, images['skulls']['orange']):
            return True
        if locate(creatureIconsImage, images['skulls']['red']):
            return True
        if locate(creatureIconsImage, images['skulls']['white']):
            return True
        if locate(creatureIconsImage, images['skulls']['yellow']):
            return True
    return False

def checkDust(content: GrayImage, creatures: CreatureList):
    for creatureIndex, creature in enumerate(creatures):
        if creature['name'] != 'Unknown':
            continue
        y = (creatureIndex * 22)
        creatureIconsImage = content[y + 2:y + 13, -38:-2]
        if locate(creatureIconsImage, images['icons']['dust'], confidence=0.75):
            creature['name'] = 'Dusted'
    return creatures

# PERF: [4.499999999296733e-06, 9.999999992515995e-07]
@njit(cache=True, fastmath=True)
def isAttackingSomeCreature(creatures: CreatureList) -> bool:
    if creatures is not None and len(creatures) > 0:
        for isBeingAttacked in creatures['isBeingAttacked']:
            if isBeingAttacked:
                return True
    return False
