from __future__ import annotations
import logging
import os
from threading import Timer
from typing import List
import pygame
import snecs
from snecs import Component
from snecs.world import default_world
import scripts.engine.core.matter
from scripts.engine.core import hourglass, state, system, utility, world
from scripts.engine.core.component import Knowledge  # for demonstration purposes
from scripts.engine.core.component import (
    Aesthetic,
    Exists,
    Identity,
    IsPlayer,
    LightSource,
    MapCondition,
    Shrine,
    Position,
    WinCondition,
)
from scripts.engine.core.ui import ui
from scripts.engine.internal import library
from scripts.engine.internal.action import register_action
from scripts.engine.internal.constant import (
    ASSET_PATH,
    DEBUG_START,
    GameState,
    GAP_SIZE,
    Height,
    MAX_SKILLS,
    RenderLayer,
    SAVE_PATH,
    SKILL_BUTTON_SIZE,
    UIElement,
)
from scripts.engine.internal.data import store
from scripts.engine.internal.definition import ActorData, TraitSpritePathsData
from scripts.engine.internal.interaction import InteractionEventSubscriber
from scripts.engine.world_objects.game_map import GameMap
from scripts.nqp.actions.affliction import BoggedDown, Flaming
from scripts.nqp.actions.behaviour import FollowPlayer, SearchAndAttack, SkipTurn
from scripts.nqp.actions.blessing import MoveFast, NoMove, AttackMove, Aftershock, SaltTheWound, KeepAnEvenKeel
from scripts.nqp.actions.skill import BasicAttack, Lightning, Lunge, Move, Splash, TarAndFeather
from scripts.nqp.processors.game import GameEventSubscriber
from scripts.nqp.ui_elements.camera import camera
from scripts.nqp.ui_elements.character_selector import CharacterSelector
from scripts.nqp.ui_elements.skill_bar import SkillBar
from scripts.nqp.ui_elements.title_screen import TitleScreen
__all__ = [
    "initialise_game",
    "goto_character_select",
    "load_game",
    "exit_game",
    "win_game",
    "register_actions",
    "get_element_rect",
]
[docs]def initialise_game():
    """
    Init the game`s required info
    """
    register_actions()
    init_subscribers()
    if DEBUG_START:
        _start_debug_game()
    else:
        state.set_new(GameState.MENU)
        goto_to_title() 
[docs]def _start_debug_game():
    """
    Create a new game and show the gamemap
    """
    # create clean snecs.world
    empty_world = snecs.World()
    world.move_world(empty_world)
    # init and save map
    game_map = GameMap("debug", 10)
    store.current_game_map = game_map
    # populate the map
    player_data = ActorData(
        key="player",
        possible_names=["player"],
        description="Player desc",
        position_offsets=[(0, 0)],
        trait_names=["shoom", "soft_tops", "dandy"],
        height=Height.MIDDLING,
    )
    game_map.generate_new_map(player_data)
    logging.info(game_map.generation_info)
    # init the player
    player = scripts.engine.core.matter.get_player()
    # tell places about the player
    hourglass.set_turn_holder(player)
    # create actor near to player
    player_pos = scripts.engine.core.matter.get_entitys_component(player, Position)
    actor_data = library.ACTORS["crocturion"]
    scripts.engine.core.matter.create_actor(actor_data, (player_pos.x, player_pos.y - 2))
    # create god
    god_data = library.GODS["the_small_gods"]
    scripts.engine.core.matter.create_god(god_data)
    # update draw position for all entities
    for entity, (aesthetic, position) in scripts.engine.core.matter.get_components([Aesthetic, Position]):
        assert isinstance(aesthetic, Aesthetic)
        assert isinstance(position, Position)
        aesthetic.draw_x, aesthetic.draw_y = (position.x, position.y)
        aesthetic.target_draw_x = aesthetic.draw_x
        aesthetic.target_draw_y = aesthetic.draw_y
    # entities load with a blank fov, update them now
    system.process_light_map()
    system.process_fov()
    system.process_tile_visibility()
    # point the camera at the player, now that FOV is updated
    camera.set_target((player_pos.x, player_pos.y), True)
    # create terrain next to the player
    scripts.engine.core.matter.create_terrain(library.TERRAIN["bog"], (player_pos.x + 1, player_pos.y))
    # loading finished, give player control
    state.set_new(GameState.GAME_MAP)
    # prompt turn actions
    hourglass.end_turn(player, 0) 
[docs]def start_game(player_data: ActorData):
    """
    Create a new game and show the gamemap
    """
    # create clean snecs.world
    empty_world = snecs.World()
    world.move_world(empty_world)
    # init and save map
    game_map = GameMap("cave", 10)
    store.current_game_map = game_map
    # populate the map
    game_map.generate_new_map(player_data)
    # init the player
    player = scripts.engine.core.matter.get_player()
    # allocate player skills
    scripts.engine.core.matter.learn_skill(player, "Lightning")
    # blessing test cases
    knowledge = scripts.engine.core.matter.get_entitys_component(player, Knowledge)
    #knowledge.add_blessing(Move, NoMove(player))
    #knowledge.add_blessing(Move, Aftershock(player))
    #knowledge.remove_blessing(BasicAttack, AttackMove)
    #knowledge.add_blessing(BasicAttack, SaltTheWound(player))
    #knowledge.add_blessing(BasicAttack, KeepAnEvenKeel(player))
    # create blessing shrine
    player_pos = scripts.engine.core.matter.get_entitys_component(player, Position)
    shrine_x = player_pos.x + 3
    shrine_y = player_pos.y
    components: List[Component] = []
    components.append(Position((shrine_x, shrine_y)))  # lets hope this doesnt spawn in a wall
    components.append(Shrine())
    traits_paths = [TraitSpritePathsData(idle=str(ASSET_PATH / "world/shrine.png"))]
    sprites = utility.build_sprites_from_paths(traits_paths)
    components.append(Aesthetic(sprites, traits_paths, RenderLayer.ACTOR, (shrine_x, shrine_y)))
    scripts.engine.core.matter.create_entity(components)
    # create win condition and place next to player
    player_pos = scripts.engine.core.matter.get_entitys_component(player, Position)
    win_x = player_pos.x + 1
    win_y = player_pos.y
    components = []
    components.append(Position((win_x, win_y)))  # lets hope this doesnt spawn in a wall
    components.append(WinCondition())
    traits_paths = [TraitSpritePathsData(idle=str(ASSET_PATH / "world/win_flag.png"))]
    sprites = utility.build_sprites_from_paths(traits_paths)
    components.append(Aesthetic(sprites, traits_paths, RenderLayer.ACTOR, (win_x, win_y)))
    scripts.engine.core.matter.create_entity(components)
    # create map change condition and place next to player
    map_x = player_pos.x + 2
    map_y = player_pos.y
    components = []
    components.append(Position((map_x, map_y)))  # lets hope this doesnt spawn in a wall
    components.append(MapCondition())
    traits_paths = [TraitSpritePathsData(idle=str(ASSET_PATH / "world/map_flag.png"))]
    sprites = utility.build_sprites_from_paths(traits_paths)
    components.append(Aesthetic(sprites, traits_paths, RenderLayer.ACTOR, (map_x, map_y)))
    scripts.engine.core.matter.create_entity(components)
    # tell places about the player
    hourglass.set_turn_holder(player)
    # create a god
    god_data = library.GODS["the_small_gods"]
    scripts.engine.core.matter.create_god(god_data)
    # show the in game screens
    # message_log = MessageLog(get_element_rect(UIElement.MESSAGE_LOG), ui.get_gui_manager())
    # ui.register_element(UIElement.MESSAGE_LOG, message_log)
    # ui.set_element_visibility(UIElement.MESSAGE_LOG, True)
    skill_bar = SkillBar(get_element_rect(UIElement.SKILL_BAR), ui.get_gui_manager())
    ui.register_element(UIElement.SKILL_BAR, skill_bar)
    ui.set_element_visibility(UIElement.SKILL_BAR, True)
    # welcome message
    ui.create_screen_message("Welcome to Not Quite Paradise")
    # FIXME - entities load before camera so they cant get their screen position.
    #  If ui loads before entities then it fails due to player not existing. Below is a hacky fix.
    for entity, (aesthetic, position) in scripts.engine.core.matter.get_components([Aesthetic, Position]):
        assert isinstance(aesthetic, Aesthetic)
        assert isinstance(position, Position)
        aesthetic.draw_x, aesthetic.draw_y = (position.x, position.y)
        aesthetic.target_draw_x = aesthetic.draw_x
        aesthetic.target_draw_y = aesthetic.draw_y
    # entities load with a blank fov, update them now
    system.process_light_map()
    system.process_fov()
    system.process_tile_visibility()
    # point the camera at the player, now that FOV is updated
    pos = scripts.engine.core.matter.get_entitys_component(player, Position)
    camera.set_target((pos.x, pos.y), True)
    # loading finished, give player control
    state.set_new(GameState.GAME_MAP)
    # prompt turn actions
    hourglass.end_turn(player, 0) 
[docs]def load_game():
    """
    Load existing game state
    """
    full_save_path = str(SAVE_PATH)
    for save_name in os.listdir(full_save_path):
        save = save_name.replace(".json", "")
        state.load_game(save)
        break
    # move camera to player
    player_pos = scripts.engine.core.matter.get_entitys_component(scripts.engine.core.matter.get_player(), Position)
    camera.set_target((player_pos.x, player_pos.y), True)
    # show UI
    skill_bar = SkillBar(get_element_rect(UIElement.SKILL_BAR), ui.get_gui_manager())
    ui.register_element(UIElement.SKILL_BAR, skill_bar)
    ui.set_element_visibility(UIElement.SKILL_BAR, True)
    # welcome message
    ui.create_screen_message("Welcome back to Not Quite Paradise")
    # loading finished, give player control
    state.set_new(GameState.GAME_MAP) 
[docs]def exit_game():
    """
    Exit the game
    """
    state.set_new(GameState.EXITING) 
[docs]def win_game():
    """
    Trigger the win game actions
    """
    state.set_new(GameState.MENU)
    ui.create_screen_message("You wonned. Huzzah.")
    # quit to main menu after a few seconds
    timer = Timer(2.0, goto_to_title)
    timer.start() 
[docs]def lose_game():
    state.set_new(GameState.MENU)
    ui.create_screen_message("You hath died. Huzzah?")
    # quit to main menu after a few seconds
    timer = Timer(2.0, goto_to_title)
    timer.start() 
[docs]def change_map():
    # AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH
    # extremely scuffed and for testing. copied a bunch of stuff from start_game()
    # entities must be forcefully deleted or components will delete lights from the new lightbox once the delete in main runs
    for entity, (_,) in list(scripts.engine.core.matter.get_components([Exists])):
        if not scripts.engine.core.matter.entity_has_component(
            entity, IsPlayer
        ) and scripts.engine.core.matter.entity_has_component(entity, Identity):
            snecs.delete_entity_immediately(entity, default_world)
    # init and save map
    game_map = GameMap("cave", 10)
    store.current_game_map = game_map
    # regenerate lights for transferred entities
    for entity, (light_source, position) in list(scripts.engine.core.matter.get_components([LightSource, Position])):
        # set up light
        radius = 2  # TODO - pull radius and colour from external data
        colour = (255, 255, 255)
        alpha = 200
        light_source.light_id = scripts.engine.core.matter.create_light([position.x, position.y], radius, colour, alpha)
    # populate the map
    game_map.generate_new_map(None)
    # get the player
    player = scripts.engine.core.matter.get_player()
    # transfer player to new spawn
    player_position = scripts.engine.core.matter.get_entitys_component(player, Position)
    new_pos = tuple(game_map.get_open_space())
    player_position.set(*new_pos)
    # visually update player position
    aesthetic = scripts.engine.core.matter.get_entitys_component(player, Aesthetic)
    if aesthetic:
        aesthetic.target_draw_x, aesthetic.target_draw_y = new_pos
        aesthetic.current_sprite = aesthetic.sprites.move
    # entities load with a blank fov, update them now
    system.process_light_map()
    system.process_fov()
    system.process_tile_visibility()
    # point the camera at the player, now that FOV is updated
    pos = scripts.engine.core.matter.get_entitys_component(player, Position)
    camera.set_target((pos.x, pos.y), True) 
############### NAVIGATION  #####################
[docs]def goto_character_select():
    """
    Create a new game
    """
    character_select = CharacterSelector(get_element_rect(UIElement.CHARACTER_SELECTOR), ui.get_gui_manager())
    ui.register_element(UIElement.CHARACTER_SELECTOR, character_select)
    ui.set_element_visibility(UIElement.CHARACTER_SELECTOR, True) 
[docs]def goto_to_title():
    """
    Quit out of the game back to the title screen
    """
    state.set_new(GameState.MENU)
    # remove any existing ui
    ui.kill_all_elements()
    # clear existing resource
    store.current_game_map = None  # TODO - add better clearing method.
    # show the title screen
    title_screen = TitleScreen(get_element_rect(UIElement.TITLE_SCREEN), ui.get_gui_manager())
    ui.register_element(UIElement.TITLE_SCREEN, title_screen)
    ui.set_element_visibility(UIElement.TITLE_SCREEN, True) 
################## INIT ##########################
[docs]def register_actions():
    """
    Register all Actions with the engine
    """
    # afflictions
    register_action(BoggedDown)
    register_action(Flaming)
    # behaviour
    register_action(SkipTurn)
    register_action(FollowPlayer)
    register_action(SearchAndAttack)
    # skills
    register_action(Move)
    register_action(BasicAttack)
    register_action(Lunge)
    register_action(TarAndFeather)
    register_action(Splash)
    register_action(Lightning)
    # blessings
    register_action(MoveFast)
    register_action(NoMove)
    register_action(AttackMove)
    register_action(Aftershock)
    register_action(SaltTheWound)
    register_action(KeepAnEvenKeel) 
[docs]def init_subscribers():
    """
    Initialise event subscribers.
    N.B. When init'd they are held in reference by the event hub and do not need to be referred to directly.
    """
    game_subscriber = GameEventSubscriber()
    interaction_subscriber = InteractionEventSubscriber() 
##################### UI ######################
[docs]def get_element_rect(element_type: UIElement) -> pygame.Rect:
    """
    Get the predefined rect for the specified element.
    """
    desired_width = ui.desired_width
    desired_height = ui.desired_height
    # Message Log
    message_width = int(desired_width * 0.31)
    message_height = int(desired_height * 0.28)
    message_x = 0
    message_y = -message_height
    # Skill Bar
    skill_width = (MAX_SKILLS * (SKILL_BUTTON_SIZE + GAP_SIZE)) + (GAP_SIZE * 2)  # gap * 2 for borders
    skill_height = SKILL_BUTTON_SIZE + (GAP_SIZE * 2)
    skill_x = (desired_width // 2) - (skill_width // 2)
    skill_y = -SKILL_BUTTON_SIZE
    # Title Screen
    title_screen_width = desired_width
    title_screen_height = desired_height
    title_screen_x = 0
    title_screen_y = 0
    # Dungeon dev view
    dungen_viewer_width = desired_width
    dungen_viewer_height = desired_height
    dungen_viewer_x = 0
    dungen_viewer_y = 0
    # Tile Info
    tile_info_width = int(desired_width * 0.19)
    tile_info_height = int(desired_height * 0.22)
    tile_info_x = -tile_info_width
    tile_info_y = -tile_info_height
    # Npc info
    npc_info_width = desired_width / 2
    npc_info_height = desired_height - (desired_height / 4)
    npc_info_x = 5
    npc_info_y = 10
    # character selector
    char_selector_width = desired_width
    char_selector_height = desired_height
    char_selector_x = 0
    char_selector_y = 0
    # blessings menu
    blessing_menu_width = desired_width / 2
    blessing_menu_height = desired_width * 0.4
    blessing_menu_x = 5
    blessing_menu_y = 10
    layout = {
        UIElement.MESSAGE_LOG: pygame.Rect((message_x, message_y), (message_width, message_height)),
        UIElement.TILE_INFO: pygame.Rect((tile_info_x, tile_info_y), (tile_info_width, tile_info_height)),
        UIElement.SKILL_BAR: pygame.Rect((skill_x, skill_y), (skill_width, skill_height)),
        UIElement.DUNGEN_VIEWER: pygame.Rect(
            (dungen_viewer_x, dungen_viewer_y), (dungen_viewer_width, dungen_viewer_height)
        ),
        UIElement.ACTOR_INFO: pygame.Rect((npc_info_x, npc_info_y), (npc_info_width, npc_info_height)),
        UIElement.BLESSING_MENU: pygame.Rect((blessing_menu_x, blessing_menu_y), (blessing_menu_width, blessing_menu_height)),
        UIElement.TITLE_SCREEN: pygame.Rect(
            (title_screen_x, title_screen_y), (title_screen_width, title_screen_height)
        ),
        UIElement.CHARACTER_SELECTOR: pygame.Rect(
            (char_selector_x, char_selector_y), (char_selector_width, char_selector_height)
        ),
    }
    return layout[element_type]