import math
import os
from typing import Iterable, List, Optional, Tuple
import pygame
from pygame.rect import Rect
from pygame.surface import Surface
import scripts.engine.core.matter
from scripts.engine.core import query, state, utility, world
from scripts.engine.core.component import Aesthetic, Position
from scripts.engine.core.ui import ui
from scripts.engine.internal.constant import (
    EventType,
    GameState,
    Height,
    InputEventType,
    InputIntent,
    TargetingMethod,
    TILE_SIZE,
    UIElement,
)
from scripts.nqp import command
from scripts.nqp.ui_elements.tile_info import TileInfo
__all__ = ["targeting"]
[docs]class Targeting:
[docs]    def __init__(self):
        self.hovered_tile = None
        self.valid_line_of_sight = False
        self.possible_los_tiles = None 
[docs]    def process_events(self, camera):
        """
        For processing local events based on camera data.
        Updates the current hovered tile based on mouse position and updates the TILE_INFO GUI element.
        """
        mx, my = pygame.mouse.get_pos()
        mx /= camera._desired_width / camera._base_width
        my /= camera._desired_height / camera._base_height
        hover_pos = (int(camera.start_x + mx / TILE_SIZE), int(camera.start_y + my / TILE_SIZE))
        try:
            new_hover = world.get_tile(hover_pos)
            if (new_hover is self.hovered_tile) == False:
                if new_hover.is_visible:
                    if not ui.has_element(UIElement.TILE_INFO):
                        tile_info: TileInfo = TileInfo(
                            command.get_element_rect(UIElement.TILE_INFO), ui.get_gui_manager()
                        )
                        ui.register_element(UIElement.TILE_INFO, tile_info)
                    tile_info = ui.get_element(UIElement.TILE_INFO)
                    tile_info.set_selected_tile_pos(hover_pos)
                    tile_info.cleanse()
                    ui.set_element_visibility(UIElement.TILE_INFO, True)
                else:
                    ui.set_element_visibility(UIElement.TILE_INFO, False)
            self.hovered_tile = new_hover
        except IndexError:
            pass 
[docs]    def process_click(self):
        """
        Process a click. This is currently just used during targeting to trigger the cast event since the camera tracks what tiles are targetable.
        TODO: Move targeting logic out of the camera.
        """
        state.set_skill_target_valid(False)
        if self.hovered_tile and self.hovered_tile.is_visible:
            if state.get_current() == GameState.TARGETING:
                active_skill_name = state.get_active_skill()
                player = scripts.engine.core.matter.get_player()
                skill = scripts.engine.core.matter.get_known_skill(player, active_skill_name)
                # set the skill target for the skill cast if the hovered tile is a valid target during line of sight targeting
                if (skill.targeting_method == TargetingMethod.DIRECTION) and self.valid_line_of_sight:
                    state.set_active_skill_target((self.hovered_tile.x, self.hovered_tile.y))
                    state.set_skill_target_valid(True)
                # cancel event triggering if the hovered target is invalid in the target mode
                if skill.targeting_method == TargetingMethod.TILE:
                    position = scripts.engine.core.matter.get_entitys_component(player, Position)
                    dif = (position.x - self.hovered_tile.x, position.y - self.hovered_tile.y)
                    if (
                        (dif not in skill.target_directions)
                        or (not self.hovered_tile.is_visible)
                        or (not world.tile_has_tags(player, self.hovered_tile, skill.target_tags))
                    ):
                        return
            event = pygame.event.Event(
                EventType.INPUT, subtype=InputEventType.TILE_CLICK, tile_pos=(self.hovered_tile.x, self.hovered_tile.y)
            )
            pygame.event.post(event) 
[docs]    def render(self, camera, target_surf: pygame.Surface):
        # targeting mode related indicator rendering
        if state.get_current() == GameState.TARGETING:
            player = scripts.engine.core.matter.get_player()
            position = scripts.engine.core.matter.get_entitys_component(player, Position)
            active_skill_name = state.get_active_skill()
            skill = scripts.engine.core.matter.get_known_skill(player, active_skill_name)
            # if this is a directional attack
            if skill.targeting_method == TargetingMethod.TILE:
                for direction in skill.target_directions:
                    target_x = position.x + direction[0]
                    target_y = position.y + direction[1]
                    tile = world.get_tile((target_x, target_y))
                    if tile.is_visible and world.tile_has_tags(player, tile, skill.target_tags):
                        option_r = pygame.Rect(
                            (target_x - camera.start_x) * TILE_SIZE,
                            (target_y - camera.start_y) * TILE_SIZE,
                            TILE_SIZE,
                            TILE_SIZE,
                        )
                        pygame.draw.rect(target_surf, (60, 70, 120), option_r, 1)
            # if this is a line of sight attack
            elif skill.targeting_method == TargetingMethod.DIRECTION:
                # regenerate possible targets if targeting mode was just entered
                if self.possible_los_tiles == None:
                    self.possible_los_tiles = []
                    state.set_skill_target_valid(False)
                    for y in range(skill.range * 2 + 1):
                        for x in range(skill.range * 2 + 1):
                            test_pos = (position.x - skill.range + x, position.y - skill.range + y)
                            valid, _ = self.check_valid_line_of_sight(test_pos)
                            if valid:
                                self.possible_los_tiles.append(test_pos)
                # check if current hovered tile is a valid target
                self.valid_line_of_sight = False
                self.valid_line_of_sight, tile_path = self.check_valid_line_of_sight(
                    (self.hovered_tile.x, self.hovered_tile.y)
                )
                # render indicators
                for tile_pos in self.possible_los_tiles:
                    highlight_r = pygame.Rect(
                        (tile_pos[0] - camera.start_x) * TILE_SIZE,
                        (tile_pos[1] - camera.start_y) * TILE_SIZE,
                        TILE_SIZE,
                        TILE_SIZE,
                    )
                    pygame.draw.rect(target_surf, (60, 70, 120), highlight_r, 1)
                if self.valid_line_of_sight:
                    for tile_pos in tile_path:
                        highlight_r = pygame.Rect(
                            (tile_pos[0] - camera.start_x) * TILE_SIZE,
                            (tile_pos[1] - camera.start_y) * TILE_SIZE,
                            TILE_SIZE,
                            TILE_SIZE,
                        )
                        pygame.draw.rect(target_surf, (50, 150, 200), highlight_r, 1)
        # empty targeting data if not in targeting mode
        else:
            self.possible_los_tiles = None
        if self.hovered_tile and self.hovered_tile.is_visible:
            hover_r = pygame.Rect(
                (self.hovered_tile.x - camera.start_x) * TILE_SIZE,
                (self.hovered_tile.y - camera.start_y) * TILE_SIZE,
                TILE_SIZE,
                TILE_SIZE,
            )
            pygame.draw.rect(target_surf, (200, 200, 0), hover_r, 1)
        target_surf.blit(pygame.transform.scale(target_surf, target_surf.get_size()), (0, 0)) 
[docs]    def check_valid_line_of_sight(self, pos: Tuple[int, int]) -> Tuple[bool, List]:
        """
        Checks if a tile is in the line of sight from the player and returns the visible path to the target tile.
        """
        # get base info and init
        player = scripts.engine.core.matter.get_player()
        position = scripts.engine.core.matter.get_entitys_component(player, Position)
        active_skill_name = state.get_active_skill()
        skill = scripts.engine.core.matter.get_known_skill(player, active_skill_name)
        valid_line_of_sight = False
        target_tile = world.get_tile(pos)
        tile_path = []
        # check if the endpoint is even valid
        if target_tile.is_visible and world.tile_has_tags(player, target_tile, skill.target_tags):
            # init some values for search
            current_pos = [position.x, position.y]
            tile_path = [current_pos.copy()]
            target_pos = list(pos)
            # get list of possible search directions
            directions_for_steps = [(-1, 0), (1, 0), (0, 1), (0, -1), (-1, -1), (1, 1), (1, -1), (-1, 1)]
            # this set is for distance calculations
            # the 0.81 reduces the weight of diagonal movement so it isn't chosen too often
            directions_for_math = [
                (-1, 0),
                (1, 0),
                (0, 1),
                (0, -1),
                (-0.81, -0.81),
                (0.81, 0.81),
                (0.81, -0.81),
                (-0.81, 0.81),
            ]
            valid = True
            # iteratively search for the bordering tile that's closest to the target
            while current_pos != target_pos:
                closest = [
                    current_pos.copy(),
                    math.sqrt((target_pos[0] - current_pos[0]) ** 2 + (target_pos[1] - current_pos[1]) ** 2),
                ]
                for i, direction in enumerate(directions_for_steps):
                    check_pos = [current_pos[0] + direction[0], current_pos[1] + direction[1]]
                    math_check_pos = [
                        current_pos[0] + directions_for_math[i][0],
                        current_pos[1] + directions_for_math[i][1],
                    ]
                    dis = math.sqrt((math_check_pos[0] - target_pos[0]) ** 2 + (math_check_pos[1] - target_pos[1]) ** 2)
                    if dis < closest[1]:  # type: ignore
                        closest = [check_pos.copy(), dis]
                tile = world.get_tile(closest[0])  # type: ignore
                # check if tile step meets targeting criteria
                if (not tile.is_visible) or (not world.tile_has_tags(player, tile, skill.cast_tags)):
                    valid = False
                current_pos = closest[0].copy()  # type: ignore
                tile_path.append(current_pos.copy())
            # determine if the final path was valid based on skill range and previous calculations
            if (len(tile_path) - 1 <= skill.range) and valid:
                valid_line_of_sight = True
        return (valid_line_of_sight, tile_path)  
if "GENERATING_SPHINX_DOCS" not in os.environ:  # when building in CI these fail
    targeting = Targeting()
else:
    targeting = ""  # type: ignore