from math import ceil, sqrt, pow, floor
from typing import Callable

from .olives_abstractobject import AbstractObject
from olives_object import Object, MovingSpeed, MessageType, SlotType, NavigationStatus, ContainedStatus, PositionStatus
from olives_types import Position2
from olives_objectcontrollable import ObjectControllable
from olives_job import Job
from olives_gamestate import GameState
from olives_object_type import ObjectType


class AbstractControllableObject(AbstractObject):
    """
    This is the base class for the controllable object behaviour (read: agents).
    Do not mistake this class with the Object and ControllableObject classes,
    as their purpose is to act as an interface to the Object C++ class.

    The "olives" prefix in the file indicates it is not meant to be edited,
    as this will most definitely result in breakage.
    """
    job_step_progress = 0
    object_id = 0

    def __init__(self, object_type_name: str, object_id: int, object_type: ObjectType):
        """ Initialises this object by dynamically adding functions for its supported actions """
        super().__init__(object_type_name, object_id, object_type)
        # it's safe to initialise it to this status, as it gets updated on every tick
        self.position_status = PositionStatus.STANDING
        for action_name in object_type.get_supported_actions():
            fun = getattr(__import__("object_" + object_type_name.lower() + "_job_" + action_name,
                                     globals(), locals(), [], 1), action_name)
            setattr(self, action_name, fun)

        # upper tick limit for logic effects
        self.effect_limits = {}
        # the current tick count for each effect
        self.effect_status = {}

    def execute_job(self, job: Job, game_state: GameState, elapsed_ticks: int,
                    obj: ObjectControllable, action: str, parameter: str):
        """ Dynamically executes the required action if the object types supports it """
        if hasattr(self, action):
            fun = getattr(self, action)
            fun(self, job, game_state, elapsed_ticks, obj, parameter)

    def reset_step_progress(self, job: Job, game_state: GameState, price: int):
        """ Resets the progress if the current step has just started """
        if job.get_step().get_ticks_elapsed() == 0:
            self.job_step_progress = 0
            game_state.reduce_funds(price)

    @staticmethod
    def update_elapsed_ticks(job: Job, elapsed_ticks: int):
        """ Update the elapsed ticks for this step since we were last called """
        overall_elapsed = job.get_step().get_ticks_elapsed()
        job.set_ticks_elapsed(overall_elapsed + elapsed_ticks)

    @staticmethod
    def check_for_completion(job: Job) -> bool:
        """ Check if we completed this step and progress to the next step if we did """
        if job.get_step().get_duration() <= job.get_step().get_ticks_elapsed():
            job.increment_step(1)
            return True
        return False

    def get_relative_progress_limited(self, job: Job, elapsed_ticks: int, total_value: int) -> int:
        """
        Returns the progress to be added to the job_step_progress
        (and be used in whatever the job is trying to achieve)
        This is only applicable for jobs that use a specific progress target
        """
        progress_per_tick = (total_value / job.get_step().get_duration())
        progress = ceil(progress_per_tick * elapsed_ticks)
        # we have a nominal target progress, if we overshoot it, do not add anything in this tick
        target_value = progress_per_tick * job.get_step().get_ticks_elapsed()
        if self.job_step_progress > target_value:
            progress = 0
        else:
            self.job_step_progress += progress
        return progress

    @staticmethod
    def get_relative_progress_unlimited(job: Job, change_rate: float) -> int:
        """
        Calculated the progress depending on a change rate and current job step.

        :param job: The current job
        :param change_rate: The target rate of change per tick
        :return: The progress delta
        """
        progress = int(job.get_step().get_ticks_elapsed() * change_rate)
        # if there is actual progress, reset the ticks so that that the
        # elapsed_ticks will work towards this threshold anew
        if progress > 0:
            job.set_ticks_elapsed(0)

        return progress

    def add_effect(self, key: str, limit: int):
        """
        Adds a new effect that may influence the object's properties
        :param key Identifier for the effect
        :param limit The number of ticks to pass until it has an effect
        """
        # initialise the new effect with an upper tick limit and the current count
        self.effect_limits[key] = limit
        self.effect_status[key] = 0

    def apply_change(self, key: str, current_value: int, elapsed_ticks: int, setter_callback,
                     has_position_status_effect: bool = True, is_addition: bool = False):
        """
        Checks if the threshold has been met to call the passed callback with the new value
        :param key The name of the attribute being modified
        :param current_value The current value of the attribute
        :param elapsed_ticks The ticks elapsed since the last run
        :param setter_callback The setter function to call should the tick threshold be reached
        :param has_position_status_effect: Whether the changes are affected by the current position status
        :param is_addition Whether the calculated delta is supposed to be position (an addition) or not
        """
        self.effect_status[key] += elapsed_ticks
        # we may slow or accelerate the effects depending on position status down the effects
        threshold = self.effect_limits[key]
        if has_position_status_effect and self.position_status != PositionStatus.STANDING:
            factor = 1
            if self.position_status == PositionStatus.SITTING:
                factor = 1.2
            elif self.position_status == PositionStatus.LYING:
                factor = 1.6
            # if this is an addition (e.g. healing) the acceleration is done by lowering the threshold
            # the opposite is the case for decelerating it for adverse effects
            threshold = (threshold / factor) if is_addition else (threshold * factor)

        if self.effect_status[key] > threshold:
            # take it consideration if the limit has been met more than once
            delta = floor(self.effect_status[key] / threshold)
            if not is_addition:
                delta *= -1
            new_value = current_value + delta
            # this expects an unsigned value, make sure we never accidentally use a lower value
            setter_callback(max(0, new_value))
            self.effect_status[key] = 0

    def handle_attribute_change(self, obj: ObjectControllable, elapsed_ticks: int, attribute: str, setter_callback,
                                has_active_case: bool, has_position_status_effect: bool):
        """
        Handles the effect a tick may have on the specified attribute. This distinguishes
        between "simple" cases that do not care about the current movement state and the ones
        affected by it (see has_active_case).
        :param obj: The wrapper around the corresponding C++ object
        :param elapsed_ticks: The ticks elapsed since the last run
        :param attribute: The name of the attribute to check
        :param setter_callback: The setter function to call should the tick threshold be reached
        :param has_active_case: Whether the attribute distinguished between movement and passive states
        :param has_position_status_effect: Whether the changes are affected by the current position status
        """
        # if there is no distinction between movement and standing still, just
        # apply the changes for the exact key and exit
        if not has_active_case:
            self.apply_change(attribute, getattr(self, attribute), elapsed_ticks, setter_callback,
                              has_position_status_effect)
            return

        # "active" change (while moving)
        if obj.get_navigation_status() == NavigationStatus.ONGOING:
            self.apply_change('moving_' + attribute, getattr(self, attribute), elapsed_ticks, setter_callback,
                              has_position_status_effect)
        else:
            # passive change (while not moving)
            self.apply_change('passive_' + attribute, getattr(self, attribute), elapsed_ticks, setter_callback,
                              has_position_status_effect)
            # always reset the timer if we are currently not moving
            # so that this effect does not get applied both active and passive
            self.effect_status['moving_' + attribute] = 0

    # noinspection PyUnboundLocalVariable
    @staticmethod
    def go_to_context_object(job: Job, game_state: GameState, obj: Object, target_type: str = 'action',
                             slot_type: SlotType = SlotType.SITTING):
        """
        Try to navigate to the next context object
        """
        # fetch the context object and get the chosen type of point (action or otherwise)
        context_obj = game_state.get_object(job.get_context().get_object_id())
        points = list()
        # after fetching the navigation targets, also define the tile the objection
        # should rotate towards after finishing navigation
        if target_type == 'action' and job.get_is_internal() is False:
            targets = context_obj.get_calculated_action_points(game_state)
            for point in targets:
                points.append({'target': point, 'rotation_target': context_obj.get_position()})
        else:
            targets = context_obj.get_calculated_entry_points(game_state, slot_type, False, -1)
            for point in targets:
                rotation_target = context_obj.get_slot_position_from_entry_point_position(game_state, slot_type,
                                                                                          point, False)
                points.append({'target': point, 'rotation_target': rotation_target})

        # try to find a valid path to the context object
        has_path = False
        own_position = obj.get_position()
        shortest_distance = -1
        for point in points:
            if obj.has_path_to(game_state, point['target']):
                has_path = True
                # do a quick check which entry point is the closest one, this is just an approximation
                # as it does not take the actual path taken into consideration
                distance = sqrt(pow(own_position.x - point['target'].x, 2) + pow(own_position.y - point['target'].y, 2))
                if shortest_distance == -1 or distance < shortest_distance:
                    shortest_distance = distance
                    target_point = point

        if has_path and target_point is not None:
            obj.set_target(target_point['target'], target_point['rotation_target'], MovingSpeed.WALKING)
        else:
            # we could not find a valid path, just pretend the job is done already
            job.clear_running(False)

    @staticmethod
    def transition_to_active_status(job: Job, obj: ObjectControllable, required_position_status: PositionStatus, step_progress: int = 2) -> bool:
        """
        Tries to transition a contained object from a passive contained status (an agent sat down/was lying down)
        to an active one (a job is being performed)

        :param job: The current job
        :param obj: The wrapper around the C++ object class
        :param required_position_status: The position status to check against (i.e. what state does the job require the agent to be in)
        :param step_progress: How many progress steps to skip as no navigation is necessary if the agent is already in the right slot
        :return:
        """
        # check if the object is already in a compatible slot in the right object
        if obj.get_position_status() == required_position_status and obj.get_container_id() == job.get_context().get_object_id():
            obj.set_contained_status(ContainedStatus.ACTIVE)
            job.increment_step(step_progress)
            return True
        return False

    def handle_context_transition_step(self, job: Job, game_state: GameState, obj: ObjectControllable,
                                       success_callback: Callable[[], bool] = None, target_type: str = 'action',
                                       slot_type: SlotType = SlotType.SITTING):
        """
        Many jobs require to to move to a context object to execute a job there. This function tries to
        handle this job step semi-automatically, a callback to execute *once* after navigation is done and
        moving on to the next job step may be passed. If none is passed, an attempt will be made to
        deduce a usable default.

        :param job: The job currently being executed, should always be supplied to job tick functions
        :param obj: The reference to the ObjectControllable instance representing this object
        :param game_state: The GameState, should always be supplied to job tick functions
        :param success_callback: Optional callback function before proceeding to the next step
        :param target_type: Whether to look for (internal) actions points or generic entry points: may be 'entry' or 'action'
        :param slot_type: What type of slot to look for when looking for entry point
        """
        # if this is the first time executing this, set the target first
        if job.get_step().get_ticks_elapsed() == 0:
            self.go_to_context_object(job, game_state, obj, target_type, slot_type)
            job.set_ticks_elapsed(1)
        elif obj.get_navigation_status() != NavigationStatus.ONGOING:
            # check if the navigation was successful
            if obj.get_navigation_status() == NavigationStatus.SUCCESS:
                # navigation navigation is completed, try to run a callback (or try to get one
                # first it if not set) and proceed to the next job step
                if success_callback is None:
                    success_callback = self.get_handle_context_transition_step_default_callback(job, game_state, obj,
                                                                                                target_type, slot_type)
                # if the callback comes back negative, abort
                if success_callback() is False:
                    job.clear_running(False)
                else:
                    job.increment_step(1)
            else:
                # navigation was aborted for some reason, skip to the end of the job
                job.clear_running(False)

    @staticmethod
    def get_handle_context_transition_step_default_callback(job: Job, game_state: GameState, obj: ObjectControllable,
                                                            target_type: str, slot_type: SlotType) -> Callable[[], bool]:
        """
        Tries to deduce a sensible default value depending on the requirements of the current job.
        If no matches are found, will return a None.
        """
        if job.get_is_internal() and slot_type == SlotType.JOB and target_type == 'action':
            def success_callback() -> bool:
                context_obj = game_state.get_object(job.get_context().get_object_id())
                return obj.try_enter_object(context_obj, game_state, SlotType.JOB, ContainedStatus.ACTIVE, "")
            return success_callback
        if job.get_is_internal() is False \
                and slot_type in [SlotType.SITTING, SlotType.LYING]\
                and target_type == 'entry':
            def success_callback() -> bool:
                context_obj = game_state.get_object(job.get_context().get_object_id())
                return obj.try_enter_object(context_obj, game_state, slot_type, ContainedStatus.PASSIVE, "")
            return success_callback

        # neither of the two supported cases occurred, return a default implemenation
        def default_func() -> bool:
            return True

        return default_func
