import cv2
from farmhash import FarmHash64
import numpy as np
import os
from PIL import ImageGrab
from typing import Callable, Union
from src.shared.typings import BBox, GrayImage


latestScreenshot = None
camera = None
obsCamera = None

# Standard-Bildquelle. "direct" liest den Bildschirm des OBS-Projektors
# mit dxcam; "obs" holt das Bild ueber die OBS-Schnittstelle.
#
# "direct" ist der Normalfall, weil es in beiden Punkten besser ist:
# schneller (kein Kodieren/Dekodieren pro Bild) und farbtreuer. Gemessen
# im Radar-Ausschnitt: ueber die OBS-Schnittstelle lagen 39 % der
# Grauwerte neben der Palette des Spiels, ueber den Projektor nur
# 17-26 %. Die Kartenerkennung vergleicht Pixelwerte - jede Faelschung
# der Farben kostet dort direkt Trefferguete.
CAPTURE_MODE = os.getenv('TIBIAPILOT_CAPTURE_MODE', 'direct').lower()


def _botDisplay():
    """Der Bildschirm, von dem der Bot sein Bild liest.

    Das ist der virtuelle Bildschirm mit dem OBS-Projektor, nicht der des
    Spiels: Vom randlosen Vollbild-Client liefert Windows nur Schwarz.
    Frueher stand hier fest output_idx=1 - das ist bei mehreren Monitoren
    der ZWEITE Ausgang. Der Bot hat dann den falschen Bildschirm gelesen
    und im Spiel nichts wiedererkannt.
    """
    from src.utils.display import getCaptureDisplay
    return getCaptureDisplay()


def _captureRegion() -> BBox:
    """Bildausschnitt fuer die Rueckfallebene (PIL)."""
    override = os.getenv('TIBIAPILOT_CAPTURE_WIDTH')
    if override is not None:
        left = int(os.getenv('TIBIAPILOT_CAPTURE_X', '0'))
        top = int(os.getenv('TIBIAPILOT_CAPTURE_Y', '0'))
        return (left, top,
                left + int(override),
                top + int(os.getenv('TIBIAPILOT_CAPTURE_HEIGHT', '1080')))
    try:
        display = _botDisplay()
    except Exception:
        return (0, 0, 1920, 1080)
    return (display['x'], display['y'],
            display['x'] + display['width'],
            display['y'] + display['height'])


def _createDirectCamera():
    import dxcam
    override = os.getenv('TIBIAPILOT_OUTPUT_IDX')
    if override is not None:
        output = int(override)
    else:
        try:
            output = int(_botDisplay()['outputIndex'])
        except Exception:
            output = 0
    return dxcam.create(
        device_idx=0,
        output_idx=output,
        output_color='BGRA',
    )


def _captureObs() -> GrayImage:
    global obsCamera
    if obsCamera is None:
        from src.utils.obsCapture import ObsCamera
        obsCamera = ObsCamera()
    return obsCamera.grab()


def _captureDirect() -> GrayImage:
    global camera
    if camera is None:
        camera = _createDirectCamera()
    screenshot = camera.grab()
    if screenshot is None:
        raise RuntimeError('Direct capture returned no frame')
    return cv2.cvtColor(screenshot, cv2.COLOR_BGRA2GRAY)


# TODO: add unit tests
def cacheObjectPosition(func: Callable) -> Callable:
    lastX = None
    lastY = None
    lastW = None
    lastH = None
    lastImgHash = None

    def inner(screenshot):
        nonlocal lastX, lastY, lastW, lastH, lastImgHash
        if lastX != None and lastY != None and lastW != None and lastH != None:
            if hashit(screenshot[lastY:lastY + lastH, lastX:lastX + lastW]) == lastImgHash:
                return (lastX, lastY, lastW, lastH)
        res = func(screenshot)
        if res is None:
            return None
        lastX = res[0]
        lastY = res[1]
        lastW = res[2]
        lastH = res[3]
        lastImgHash = hashit(
            screenshot[lastY:lastY + lastH, lastX:lastX + lastW])
        return res
    return inner


# TODO: add unit tests
def hashit(arr: np.ndarray) -> int:
    return FarmHash64(np.ascontiguousarray(arr))


# TODO: add unit tests
def locate(compareImage: GrayImage, img: GrayImage, confidence: float = 0.85, type = cv2.TM_CCOEFF_NORMED) -> Union[BBox, None]:
    match = cv2.matchTemplate(compareImage, img, type)
    res = cv2.minMaxLoc(match)
    if res[1] <= confidence:
        return None
    return res[3][0], res[3][1], len(img[0]), len(img)


# TODO: add unit tests
def locateMultiple(compareImg: GrayImage, img: GrayImage, confidence: float = 0.85) -> Union[BBox, None]:
    match = cv2.matchTemplate(compareImg, img, cv2.TM_CCOEFF_NORMED)
    loc = np.where(match >= confidence)
    resultList = []
    for pt in zip(*loc[::-1]):
        resultList.append((pt[0], pt[1], len(compareImg[0]), len(compareImg)))
    return resultList


# TODO: add unit tests
def getScreenshot() -> GrayImage:
    global latestScreenshot
    # Reihenfolge nach eingestelltem Modus. OBS ist der zuverlaessige Weg
    # (auch virtueller Bildschirm); dxcam nur auf echten Monitoren; PIL als
    # letzte Rueckfallebene. Schlaegt der bevorzugte Weg fehl, wird der
    # naechste versucht, damit ein einzelner Aussetzer den Bot nicht
    # blind macht.
    if CAPTURE_MODE == 'direct':
        wege = (_captureDirect, _captureObs)
    else:
        wege = (_captureObs, _captureDirect)

    for weg in wege:
        try:
            bild = weg()
            if bild is not None and bild.mean() > 1:
                latestScreenshot = bild
                return latestScreenshot
        except Exception:
            continue

    screenshot = np.asarray(ImageGrab.grab(
        bbox=_captureRegion(),
        all_screens=True,
    ))
    latestScreenshot = cv2.cvtColor(screenshot, cv2.COLOR_RGB2GRAY)
    return latestScreenshot
