from math import radians

import bge
from bge import events
from bge.logic import keyboard
from mathutils import Euler, Vector

from .math_base import XAXIS, YAXIS, ZAXIS

EPSILON = 0.001
STEP_SIZE_M = 0.25
TURN_ACTION_LAYER = 0
STEP_ACTION_LAYER = 2
RUN_SPEED_BOOST = 2.5
TURN_SPEED_BOOST = 1.5
ACTIVE = bge.logic.KX_SENSOR_ACTIVE
BLEND_ADD = bge.logic.KX_ACTION_BLEND_ADD


def init():
    bge.logic.mouse.visible = False


class Modifiers:
    def __init__(self, run=False):
        self.run = run


def step(c):
    char_root = c.owner
    input_vector = get_input_vector()
    modifiers = get_modifiers()

    snap_to_grid(char_root)
    snap_orientation(char_root)

    if can_play_rest_animation(char_root):
        play_rest_animation(char_root)

    if not input_vector.magnitude > EPSILON:
        return
    if not can_step_at_this_time(char_root):
        return

    if can_turn_at_this_time(char_root):
        rotate_on_axis(char_root, input_vector, modifiers)
    try_to_move(char_root, input_vector, modifiers)


def can_play_rest_animation(char_root):
    armature = char_root.children['arm']
    return not armature.isPlayingAction(TURN_ACTION_LAYER)


def can_step_at_this_time(char_root):
    armature = char_root.children['arm']
    return not armature.isPlayingAction(STEP_ACTION_LAYER)


def can_turn_at_this_time(char_root):
    armature = char_root.children['arm']
    action = armature.getActionName(TURN_ACTION_LAYER)
    return not action.startswith('turn')
    # return not armature.isPlayingAction(TURN_ACTION_LAYER)


def rotate_on_axis(char_root, input_vector, modifiers):
    if input_vector.x > 0.5:
        turn_direction = 'RIGHT'
    elif input_vector.x < -0.5:
        turn_direction = 'LEFT'
    else:
        return

    rot_angle = -90 if turn_direction == 'RIGHT' else 90
    char_root.applyRotation(Euler((0, 0, radians(rot_angle)), 'XYZ'), True)
    play_turn_animation(char_root, turn_direction, modifiers.run)


def play_rest_animation(char_root):
    armature = char_root.children['arm']
    armature.playAction('rest', 1, 12, TURN_ACTION_LAYER)


def play_turn_animation(char_root, turn_direction, fast):
    if turn_direction == 'RIGHT':
        action = 'turn_right'
        end_frame = 11
    elif turn_direction == 'LEFT':
        action = 'turn_left'
        end_frame = 11
    else:
        return

    speed = TURN_SPEED_BOOST if fast else 1
    armature = char_root.children['arm']
    armature.playAction(action, 1, end_frame, TURN_ACTION_LAYER, speed=speed)


def try_to_move(char_root, input_vector, modifiers):
    if input_vector.y > 0.5:
        forward = True
    elif input_vector.y < -0.5:
        forward = False
    else:
        return

    feelers = get_feelers(char_root, forward)
    step_mode = get_step_mode(*feelers)
    apply_movement(char_root, forward, step_mode)
    play_animation(char_root, forward, step_mode, modifiers.run)


def apply_movement(char_root, forward, step_mode):
    if step_mode == 'STEP_ONTO_FLOOR':
        step_onto_floor(char_root, forward)
    elif step_mode == 'STEP_DOWN':
        step_down(char_root, forward)
    elif step_mode == 'STEP_ONTO_WALL':
        step_onto_wall(char_root, forward)
    elif step_mode == 'STEP_ONTO_CLIFF':
        step_onto_cliff(char_root, forward)


def snap_to_grid(char_root):
    zaxis = char_root.getAxisVect(ZAXIS)
    half_step = STEP_SIZE_M / 2
    if abs(zaxis.dot(XAXIS)) > 0.5:
        # Z aligned with world X
        origin = Vector((0, half_step, half_step))
    elif abs(zaxis.dot(YAXIS)) > 0.5:
        # Z aligned with world Y
        origin = Vector((half_step, 0, half_step))
    else:
        # Z aligned with world Z
        origin = Vector((half_step, half_step, 0))

    offset_in_m = char_root.worldPosition - origin
    offset_in_grid_units = offset_in_m / STEP_SIZE_M
    offset_in_grid_units = round_vec(offset_in_grid_units)
    offset_in_m = offset_in_grid_units * STEP_SIZE_M
    char_root.worldPosition = origin + offset_in_m


def snap_orientation(char_root):
    world_axes = (XAXIS, YAXIS, ZAXIS)
    local_axes = [
        char_root.getAxisVect(axis)
        for axis in world_axes]

    for local_axis_index, local_axis in enumerate(local_axes):
        for world_axis in world_axes:
            try_align_axis(char_root, local_axis, world_axis, local_axis_index)


def try_align_axis(char_root, local_axis, world_axis, axis_index):
    if local_axis.dot(world_axis) > 0.5:
        char_root.alignAxisToVect(world_axis, axis_index)
    elif local_axis.dot(world_axis) < -0.5:
        char_root.alignAxisToVect(-world_axis, axis_index)


def round_vec(vec):
    rounded = tuple(round(c) for c in vec)
    return Vector(rounded)


def play_animation(char_root, forward, step_mode, fast):
    if step_mode == 'STEP_ONTO_FLOOR':
        if forward:
            action = 'flip_forward'
        else:
            action = 'flip_backward'
        end_frame = 13
    elif step_mode == 'STEP_DOWN':
        if forward:
            action = 'flip_down_forward'
        else:
            action = 'flip_down_backward'
        end_frame = 13
    elif step_mode == 'STEP_ONTO_WALL':
        if forward:
            action = 'flip_wall_forward'
        else:
            action = 'flip_wall_backward'
        end_frame = 9
    elif step_mode == 'STEP_ONTO_CLIFF':
        if forward:
            action = 'flip_cliff_forward'
        else:
            action = 'flip_cliff_backward'
        end_frame = 14
    else:
        return

    speed = RUN_SPEED_BOOST if fast else 1
    armature = char_root.children['arm']
    armature.playAction(action, 1, end_frame, STEP_ACTION_LAYER, speed=speed)


def step_onto_floor(char_root, forward):
    print('stepping onto floor')
    signed_distance_y = STEP_SIZE_M
    if not forward:
        signed_distance_y *= -1
    char_root.applyMovement(Vector((0, signed_distance_y, 0)), True)


def step_down(char_root, forward):
    print('stepping down')
    signed_distance_y = STEP_SIZE_M
    if not forward:
        signed_distance_y *= -1
    distance_z = -STEP_SIZE_M
    char_root.applyMovement(Vector((0, signed_distance_y, distance_z)), True)


def step_onto_wall(char_root, forward):
    print('stepping onto wall')
    signed_distance_y = STEP_SIZE_M / 2
    if not forward:
        signed_distance_y *= -1
    distance_z = STEP_SIZE_M / 2
    char_root.applyMovement(Vector((0, signed_distance_y, distance_z)), True)

    rot_angle = 90 if forward else -90
    char_root.applyRotation(
        Euler((radians(rot_angle), 0, 0), 'XYZ'), True)


def step_onto_cliff(char_root, forward):
    print('stepping onto cliff')
    signed_distance_y = STEP_SIZE_M / 2
    if not forward:
        signed_distance_y *= -1
    distance_z = -STEP_SIZE_M / 2
    char_root.applyMovement(Vector((0, signed_distance_y, distance_z)), True)

    rot_angle = -90 if forward else 90
    char_root.applyRotation(
        Euler((radians(rot_angle), 0, 0), 'XYZ'), True)


def get_feelers(char_root, forward: bool):
    if forward:
        feel_horiz = char_root.children['feel_fwd']
        feel_down = char_root.children['feel_fwd_down']
        feel_under = char_root.children['feel_fwd_under']
    else:
        feel_horiz = char_root.children['feel_back']
        feel_down = char_root.children['feel_back_down']
        feel_under = char_root.children['feel_back_under']
    return feel_horiz, feel_down, feel_under


def get_step_mode(feel_horiz, feel_down, feel_under):
    hit_ob, hit_loc, hit_nor, dist = feel(feel_horiz)
    if dist < STEP_SIZE_M:
        if not can_flip_onto(hit_ob):
            return 'DO_NOT_STEP'
        return 'STEP_ONTO_WALL'

    hit_ob, hit_loc, hit_nor, dist = feel(feel_down)
    if hit_ob:
        if not can_step_onto(hit_ob):
            return 'DO_NOT_STEP'
        elif dist < STEP_SIZE_M:
            return 'STEP_ONTO_FLOOR'
        elif dist < STEP_SIZE_M * 2:
            return 'STEP_DOWN'

    hit_ob, hit_loc, hit_nor, dist = feel(feel_under)
    if dist < STEP_SIZE_M:
        if not can_flip_onto(hit_ob):
            return 'DO_NOT_STEP'
        return 'STEP_ONTO_CLIFF'

    return 'DO_NOT_STEP'


def can_flip_onto(surface):
    if not can_step_onto(surface):
        return False
    elif 'noflip' in surface:
        return False
    return True


def can_step_onto(surface):
    if 'nostep' in surface:
        return False
    return True


def feel(feeler):
    zaxis = feeler.getAxisVect(ZAXIS)
    to = feeler.worldPosition + zaxis
    hit_ob, hit_loc, hit_nor = feeler.rayCast(to, None, STEP_SIZE_M * 2)
    if hit_ob:
        distance = (hit_loc - feeler.worldPosition).magnitude
    else:
        distance = float('inf')
    return hit_ob, hit_loc, hit_nor, distance


def get_modifiers():
    modifiers = Modifiers()
    modifiers.run = (
        keyboard.events[events.LEFTSHIFTKEY] == ACTIVE or
        keyboard.events[events.RIGHTSHIFTKEY] == ACTIVE)

    return modifiers


def get_input_vector():
    input_vector = Vector((0, 0))
    if key_is_active(events.UPARROWKEY, events.WKEY):
        input_vector.y += 1
    if key_is_active(events.DOWNARROWKEY, events.SKEY):
        input_vector.y -= 1
    if key_is_active(events.RIGHTARROWKEY, events.DKEY):
        input_vector.x += 1
    if key_is_active(events.LEFTARROWKEY, events.AKEY):
        input_vector.x -= 1

    if abs(input_vector.x) > 1:
        input_vector.x /= input_vector.x
    if abs(input_vector.y) > 1:
        input_vector.y /= input_vector.y
    return input_vector


def key_is_active(*keys):
    return any(
        keyboard.events[key_] == ACTIVE
        for key_ in keys
    )
