use crate::Player;
use fyrox::graph::SceneGraph;
use fyrox::{
    core::{
        algebra::{Point2, Vector2, Vector3},
        impl_component_provider,
        pool::Handle,
        reflect::prelude::*,
        uuid_provider,
        visitor::prelude::*,
    },
    scene::{
        animation::spritesheet::SpriteSheetAnimation,
        collider::{BitMask, InteractionGroups},
        dim2::{
            physics::{Intersection, RayCastOptions},
            rectangle::Rectangle,
            rigidbody::RigidBody,
        },
        graph::Graph,
        node::Node,
    },
    script::{ScriptContext, ScriptTrait},
};

impl_component_provider!(Enemy,);
uuid_provider!(Enemy = "a5671d19-9f1a-4286-8486-add4ebaadaec");

#[derive(Debug, Clone, Default, Copy)]
enum Animations {
    #[default]
    Idle = 0,
    Run = 1,
    Fight = 2,
    Dead = 3,
    Block = 4,
}

const DISTANCE_TO_FIGHT: f32 = 0.5;
const DISTANCE_TO_VIEW: f32 = 5.0;
const END_MAP_LEFT: f32 = -30.0;
const END_MAP_RIGHT: f32 = 30.0;
const SPEED: f32 = 3.1;

#[derive(Visit, Reflect, Debug, Clone, Default)]
pub struct Enemy {
    // pub sprite: Handle<Node>,
    move_left: bool,
    move_right: bool,
    jump: bool,
    pub animations: Vec<SpriteSheetAnimation>,
    fight: bool,
    win: bool,
    dead: bool,
    block: bool,

    #[reflect(hidden)]
    #[visit(skip)]
    current_animation: Animations,

    handle: Handle<Node>,
    player_handle: Handle<Node>,
    player_collider: Handle<Node>,

    attack_damage: f32,
    attack_timer: f32,
    attack_speed: f32,
    pub x: f32,
    pub y: f32,
}
impl Enemy {
    fn init(&mut self) {
        self.attack_damage = 100.0;
        self.attack_speed = 0.1;
    }

    fn round(&self, a: f32) -> i32 {
        return a.round() as i32;
    }

    pub fn take_damage(&mut self, _damage: &f32) {
        if self.dead {
            return;
        }
        self.block = true;
    }

    fn intersections_box(&self, graph: &mut Graph) -> bool {
        if self.dead {
            return false;
        }
        let self_node = match graph.try_get_mut(self.handle) {
            Some(node) => node,
            None => return false,
        };
        let self_position = self_node.global_position();

        // Cast a ray from *this* node in the direction of the player node
        let mut buffer = Vec::<Intersection>::new();

        graph.physics2d.cast_ray(
            RayCastOptions {
                ray_origin: Point2::new(self_position.x, self_position.y),
                ray_direction: Vector2::new(0.0, 1.0),
                max_len: 0.5,
                groups: InteractionGroups::default(),
                // Sort the results by distance
                sort_results: true,
            },
            // Store the collisions in the vector
            &mut buffer,
        );
        if buffer.len() == 0 {
            return false;
        }
        for i in 0..buffer.len() {
            if self.player_handle == buffer[i].collider {
                continue;
            }
            if buffer[i].toi == 0.0 {
                continue;
            }
            if self.round(buffer[i].position[0]) != self.round(self_position.x) {
                continue;
            }
            if self.round(buffer[i].position[1]) == self.round(self_position.y) {
                continue;
            }
            return true;
        }
        return false;
    }

    fn intersections_player(&self, graph: &mut Graph, max_len: f32) -> Option<Vec<Intersection>> {
        if self.dead {
            return None;
        }
        // Get *this* node instance
        let self_node = match graph.try_get_mut(self.handle) {
            Some(node) => node,
            None => return None,
        };
        let self_position = self_node.global_position();

        // Get the player node
        let player_node = match graph.try_get_mut(self.player_handle) {
            Some(node) => node,
            None => return None,
        };

        // Get the current position of *this* node and the player node
        let player_position = player_node.global_position();

        // Calculate the direction vector from *this* node to the player node
        let direction = player_position - self_position;

        // Cast a ray from *this* node in the direction of the player node
        let mut buffer = Vec::<Intersection>::new();

        graph.physics2d.cast_ray(
            RayCastOptions {
                ray_origin: Point2::new(self_position.x, self_position.y),
                ray_direction: Vector2::new(direction.x, direction.y),
                max_len: max_len,
                groups: InteractionGroups::new(
                    // Only collide with the player
                    BitMask(0b1000_0000_0000_0000_0000_0000_0000_0000),
                    BitMask(0b0111_1111_1111_1111_1111_1111_1111_1111),
                ),
                // groups: InteractionGroups::default(),
                // Sort the results by distance
                sort_results: true,
            },
            // Store the collisions in the vector
            &mut buffer,
        );
        if buffer.len() == 0 {
            return None;
        }
        return Some(buffer);
    }

    fn attack_player(&mut self, ctx: &mut ScriptContext) {
        if self.win {
            return;
        }
        if !self.fight {
            return;
        }
        let player_script = ctx
            .scene
            .graph
            .try_get_script_of_mut::<Player>(self.player_handle)
            .unwrap();
        if player_script.get_health() <= 0.0 {
            self.fight = false;
            self.block = false;
            self.win = true;
            return;
        }
        self.block = false;
        player_script.take_damage(&self.attack_damage);
    }

    fn get_player_position(&self, context: &mut ScriptContext) -> Vector3<f32> {
        let player_binding = context.scene.graph.try_get_mut(self.player_handle).unwrap();
        let player_rigid_body = player_binding.cast::<RigidBody>();
        let player_rigid_data = match player_rigid_body {
            Some(rigid_body) => rigid_body,
            None => panic!("wrong player position"),
        };

        return Vector3::new(
            player_rigid_data.local_transform().position().x,
            player_rigid_data.local_transform().position().y,
            player_rigid_data.local_transform().position().z,
        );
    }

    fn distance_to_player(&self, context: &mut ScriptContext) -> f32 {
        let player_position = self.get_player_position(context);

        let enemy_binding = context.scene.graph.try_get_mut(context.handle).unwrap();
        let enemy_rigid_body = enemy_binding.cast::<RigidBody>();
        let enemy_rigid_data = match enemy_rigid_body {
            Some(rigid_data) => rigid_data,
            None => return 0.0,
        };

        // TODO: if player is places abroad maps - stop activity
        // if player_position.x <= END_MAP_LEFT || player_position.x >= END_MAP_RIGHT {
        //     return 100.0;
        // }

        let enemy_position = enemy_rigid_data.local_transform().position();
        if player_position.y > enemy_position.y {
            return DISTANCE_TO_VIEW * 2.0;
        }
        return player_position.x - enemy_position.x;
    }

    fn get_speed(&self, context: &mut ScriptContext) -> f32 {
        if self.dead {
            return 0.0;
        }
        if self.win {
            return 0.0;
        }
        let collaider = self.intersections_player(&mut context.scene.graph, DISTANCE_TO_VIEW);
        let mut see_player: bool = false;
        if let Some(collaider_data) = collaider {
            for i in 0..collaider_data.len() {
                if collaider_data[i].toi == 0.0 {
                    continue;
                }
                if collaider_data[i].collider == self.player_collider {
                    see_player = true;
                }
                break;
            }
        }
        if !see_player {
            return 0.0;
        }

        let player_position = self.get_player_position(context);
        let enemy_rigid_body = context.scene.graph[context.handle]
            .cast_mut::<RigidBody>()
            .unwrap();
        if enemy_rigid_body.global_position().x <= END_MAP_LEFT {
            return 0.0;
        }
        if enemy_rigid_body.global_position().x >= END_MAP_RIGHT {
            return 0.0;
        }
        let mut dif_x = player_position.x - enemy_rigid_body.global_position().x;
        if enemy_rigid_body.global_position().x > player_position.x {
            dif_x = player_position.x - enemy_rigid_body.global_position().x;
        }
        if dif_x < 0.0 {
            dif_x *= -1.0;
        }
        if player_position.y > enemy_rigid_body.global_position().y && dif_x < 1.0  {
            if player_position.x > enemy_rigid_body.global_position().x {
                return  SPEED * -1.0;
            }
            if player_position.x < enemy_rigid_body.global_position().x {
                return  SPEED;
            }
        }
        if enemy_rigid_body.global_position().x < END_MAP_LEFT + 3.0 {
            return SPEED / 2.0;
        }
        if enemy_rigid_body.global_position().x > END_MAP_RIGHT - 3.0 {
            return SPEED / 2.0;
        }

        let distance = self.distance_to_player(context);
        if distance > DISTANCE_TO_VIEW {
            return 0.0;
        }

        if distance > DISTANCE_TO_FIGHT {
            return SPEED;
        }
        if distance < DISTANCE_TO_FIGHT * -1.0 {
            return SPEED * -1.0;
        }
        return 0.0;
    }

    fn change_orientation(&self, context: &mut ScriptContext) {
        let x_speed = self.get_speed(context);
        if x_speed == 0.0 {
            return;
        }
        let sprite = context.scene.graph.try_get_mut(context.handle);
        match sprite {
            Some(sprite_data) => {
                // It is always a good practice to check whether the handles are valid, at this point we don't know
                // for sure what's the value of the `sprite` field. It can be unassigned and the following code won't
                // execute. A simple `context.scene.graph[self.sprite]` would just panicked in this case.
                let local_transform: &mut fyrox::scene::transform::Transform =
                    sprite_data.local_transform_mut();
                let current_scale = **local_transform.scale();
                local_transform.set_scale(Vector3::new(
                    // Just change X scaling to mirror player's sprite.
                    current_scale.x.copysign(-x_speed),
                    current_scale.y,
                    current_scale.z,
                ));
            }
            None => {}
        }
    }

    fn move_do(&mut self, context: &mut ScriptContext) {
        let x_speed = self.get_speed(context);
        if x_speed > 0.0 {
            self.block = false;
        }
        let enemy_rigid_body = context.scene.graph[context.handle].cast_mut::<RigidBody>();
        if let Some(enemy_rigid_data) = enemy_rigid_body {
            enemy_rigid_data.set_lin_vel(Vector2::new(x_speed, -1.0));
        }
    }

    fn animate_choose(&mut self, context: &mut ScriptContext) {
        self.current_animation = Animations::Idle;
        if self.win {
            return;
        }
        if self.block {
            self.current_animation = Animations::Block;
            return;
        }
        if self.dead {
            self.current_animation = Animations::Dead;
            return;
        }
        if self.get_speed(context) != 0.0 {
            self.current_animation = Animations::Run;
            return;
        }
        if self.fight {
            self.current_animation = Animations::Fight;
            return;
        }
    }

    fn set_fight(&mut self, context: &mut ScriptContext) {
        if self.win {
            return;
        }
        if self.dead {
            return;
        }
        if self.block {
            return;
        }
        let distance = self.distance_to_player(context);
        self.fight = distance < DISTANCE_TO_FIGHT && distance > -1.0 * DISTANCE_TO_FIGHT;
        if !self.fight {
            self.attack_timer = 0.0;
            return;
        }
        self.attack_timer += context.dt;
        if self.attack_timer >= self.attack_speed {
            self.attack_player(context);
            self.attack_timer = 0.0;
            return;
        }
        self.fight = false;
    }

    pub fn animation_do(&mut self, context: &mut ScriptContext) {
        self.animate_choose(context);
        let cur_anim = self.current_animation as usize;
        let current_animation = self.animations.get_mut(cur_anim);
        match current_animation {
            Some(animation_data) => {
                {
                    let graph_ctx = context.scene.graph.begin_multi_borrow();
                    let enemy_rigid_data = match graph_ctx.try_get(context.handle) {
                        Ok(rigid_body) => rigid_body,
                        Err(_) => return,
                    };
                    for child in enemy_rigid_data.children().iter() {
                        if let Ok(mut enemy) = graph_ctx.try_get_mut(*child) {
                            if !enemy.is_rectangle() {
                                continue;
                            }
                            let sprite: &mut Rectangle = enemy.cast_mut::<Rectangle>().unwrap();
                            animation_data.update(context.dt);
                            sprite
                                .material()
                                .data_ref()
                                .set_texture(&"diffuseTexture".into(), animation_data.texture())
                                .unwrap();
                            sprite.set_uv_rect(
                                animation_data.current_frame_uv_rect().unwrap_or_default(),
                            );
                            break;
                        }
                    }
                }

                animation_data.update(context.dt);
                if let Some(sprite) = context
                    .scene
                    .graph
                    .try_get_mut(context.handle)
                    .and_then(|n| n.cast_mut::<Rectangle>())
                {
                    sprite
                        .material()
                        .data_ref()
                        .set_texture(&"diffuseTexture".into(), animation_data.texture())
                        .unwrap();
                    sprite.set_uv_rect(animation_data.current_frame_uv_rect().unwrap_or_default());
                }
            }
            None => {}
        }
    }
}
impl ScriptTrait for Enemy {
    fn on_init(&mut self, context: &mut ScriptContext) {
        self.init();
        // Store reference to *this* instance of the enemy node
        self.handle = context.handle;

        // Find the Player node
        match context.scene.graph.find_by_name_from_root("Player") {
            // (Handle<Node>, Node)
            Some(handle) => {
                // If found, store the handle
                self.player_handle = handle.0;

                // Find and store the Player's collider node handle
                for child in handle.1.children().iter() {
                    self.player_collider = *child;
                    break;
                }
            }
            None => {}
        }
    }

    fn on_update(&mut self, context: &mut ScriptContext) {
        if self.intersections_box(&mut context.scene.graph) {
            self.dead = true
        }
        self.set_fight(context);
        self.change_orientation(context);
        self.animation_do(context);
        self.move_do(context);
    }
}
