import json
from pathlib import Path

import cv2
import numpy as np

from src.gameplay.core.waypoint import generateFloorWalkpoints
from src.gameplay.utils import releaseKeys
from src.repositories.radar.core import getCoordinate
from ...typings import Context


LAST_COORDINATE_PATH = Path('.pilotng-last-coordinate.json')
RESUME_PATH = Path('.pilotng-resume.json')
TILE_PIXELS = 64
MIN_MOTION_RESPONSE = 0.4
MIN_TILE_MOTION = 48


def _visualCoordinate(context: Context, coordinate):
    previous = context['ng_radar']['previousCoordinate']
    gameWindow = context.get('gameWindow', {})
    bounds = gameWindow.get('coordinate')
    if bounds is None:
        return coordinate
    x, y, width, height = bounds
    sample = context['ng_screenshot'][y:y + height, x:x + width]
    previousSample = context['ng_radar'].get('previousGameWindowSample')
    context['ng_radar']['previousGameWindowSample'] = sample.copy()
    if previous is None:
        context['ng_radar']['motionPixels'] = [0.0, 0.0]
        return coordinate

    if coordinate is not None and coordinate[2] != previous[2]:
        if (abs(coordinate[2] - previous[2]) == 1 and
                abs(coordinate[0] - previous[0]) <= 2 and
                abs(coordinate[1] - previous[1]) <= 2):
            context['ng_radar']['motionPixels'] = [0.0, 0.0]
            return coordinate
        coordinate = None

    if previousSample is None or previousSample.shape != sample.shape:
        context['ng_radar']['motionPixels'] = [0.0, 0.0]
        return previous

    shift, response = cv2.phaseCorrelate(
        previousSample.astype(np.float32), sample.astype(np.float32),
    )
    if (response < MIN_MOTION_RESPONSE or
            max(abs(shift[0]), abs(shift[1])) > TILE_PIXELS * 1.5):
        return previous

    horizontal, vertical = shift
    pixels = context['ng_radar'].setdefault('motionPixels', [0.0, 0.0])
    if abs(horizontal) >= max(0.5, abs(vertical) * 2):
        pixels[0] -= horizontal
        pixels[1] = 0.0
    elif abs(vertical) >= max(0.5, abs(horizontal) * 2):
        pixels[0] = 0.0
        pixels[1] -= vertical
    else:
        return previous

    deltaX = deltaY = 0
    if abs(pixels[0]) >= MIN_TILE_MOTION:
        deltaX = 1 if pixels[0] > 0 else -1
        pixels[0] -= deltaX * TILE_PIXELS
    if abs(pixels[1]) >= MIN_TILE_MOTION:
        deltaY = 1 if pixels[1] > 0 else -1
        pixels[1] -= deltaY * TILE_PIXELS
    return (
        previous[0] + deltaX,
        previous[1] + deltaY,
        previous[2],
    )


# TODO: add unit tests
def setRadarMiddleware(context: Context) -> Context:
    waypoints = context.get('ng_cave', {}).get('waypoints', {}).get('items')
    previous = context['ng_radar']['previousCoordinate']
    coordinate = getCoordinate(
        context['ng_screenshot'],
        previousCoordinate=previous,
        waypoints=waypoints,
    )
    if coordinate is None and previous is None and RESUME_PATH.exists():
        try:
            resume = json.loads(RESUME_PATH.read_text(encoding='utf-8-sig'))
            index = resume['waypointIndex']
            candidate = resume['coordinate']
            if (waypoints and 0 <= index < len(waypoints) and
                    isinstance(candidate, list) and len(candidate) == 3):
                coordinate = tuple(candidate)
                context['ng_radar']['previousCoordinate'] = coordinate
                context['ng_cave']['waypoints']['currentIndex'] = index
                RESUME_PATH.unlink()
        except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
            pass
    if coordinate is None and previous is None and LAST_COORDINATE_PATH.exists():
        try:
            candidate = json.loads(LAST_COORDINATE_PATH.read_text(encoding='utf-8'))
            if isinstance(candidate, list) and len(candidate) == 3:
                coordinate = getCoordinate(
                    context['ng_screenshot'],
                    previousCoordinate=candidate,
                    waypoints=waypoints,
                )
        except (OSError, TypeError, ValueError, json.JSONDecodeError):
            pass
    coordinate = _visualCoordinate(context, coordinate)
    context['ng_radar']['coordinate'] = coordinate
    if coordinate is None:
        context = releaseKeys(context)
    return context


# TODO: add unit tests
def setWaypointIndexMiddleware(context: Context) -> Context:
    coordinate = context['ng_radar']['coordinate']
    waypoints = context['ng_cave']['waypoints']
    if coordinate is not None and waypoints['currentIndex'] is None and waypoints['items']:
        candidates = sorted(
            (
                (abs(item['coordinate'][0] - coordinate[0]) +
                 abs(item['coordinate'][1] - coordinate[1]), index, item)
                for index, item in enumerate(waypoints['items'])
                if item['coordinate'][2] == coordinate[2]
            ),
            key=lambda candidate: candidate[0],
        )
        for _, index, item in candidates:
            if item['coordinate'] == list(coordinate) or generateFloorWalkpoints(
                    coordinate, item['coordinate']):
                waypoints['currentIndex'] = index
                break
        if waypoints['currentIndex'] is None and candidates:
            waypoints['currentIndex'] = candidates[0][1]
    return context
