use std::collections::VecDeque;

use crate::msg::Message;
use crate::Enemy;
use fyrox::{
    core::{
        algebra::{Point2, Vector2, Vector3},
        color::Color,
        impl_component_provider,
        log::Log,
        pool::Handle,
        reflect::prelude::*,
        uuid_provider,
        visitor::prelude::*,
    },
    event::{ElementState, Event, TouchPhase, WindowEvent},
    keyboard::{KeyCode, PhysicalKey},
    scene::{
        animation::spritesheet::SpriteSheetAnimation,
        base::BaseBuilder,
        collider::InteractionGroups,
        dim2::{
            physics::{Intersection, RayCastOptions},
            rectangle::{Rectangle, RectangleBuilder},
            rigidbody::RigidBody,
        },
        node::Node,
        transform::TransformBuilder,
    },
    script::{ScriptContext, ScriptMessageContext, ScriptMessagePayload, ScriptTrait},
};

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

    #[reflect(hidden)]
    #[visit(skip)]
    current_animation: VecDeque<u32>,

    start_y_position: f32,
    start_fight_last: f32,
    prev_y_velosity: f32,
    damage: f32,

    pub window_height: u32, // TODO: need change place
    pub window_width: u32,

    // Health Bar
    max_health: f32,
    health: f32,
    health_bar_background: Handle<Node>,
    health_bar_progress: Handle<Node>,
}

#[derive(Clone, Copy, PartialEq)]
enum Animations {
    Steps = 0,
    Idle = 1,
    Jump = 2,
    Fight = 3,
    Dead = 4,
}

uuid_provider!(Player = "c5671d19-9f1a-4286-8486-add4ebaadaec");

impl Player {
    const PLAYER_STARTING_HEALTH: f32 = 100.0;
    const MAX_HEALTH_BAR_WIDTH: f32 = 0.5;

    pub fn init(&mut self, context: &mut ScriptContext) {
        let rigid_body = context.scene.graph[context.handle].cast_mut::<RigidBody>();
        match rigid_body {
            Some(rigid_data) => {
                self.start_y_position = rigid_data.global_position().y;
            }
            None => {}
        }
        self.health_bar(context);
    }

    fn health_bar(&mut self, context: &mut ScriptContext) {
        // Create the healthbar background
        let health_bar_background = RectangleBuilder::new(
            BaseBuilder::new()
                .with_name("HealthBarBackground")
                .with_local_transform(
                    TransformBuilder::new()
                        // Resize the healthbar so it is wide and short
                        .with_local_scale(Vector3::new(
                            Self::MAX_HEALTH_BAR_WIDTH,
                            Self::MAX_HEALTH_BAR_WIDTH / 4.,
                            1.,
                        ))
                        // Move the pivot center to the top left of the healthbar
                        // This is so it can be scaled from the left to the right
                        .with_scaling_pivot(Vector3::new(
                            Self::MAX_HEALTH_BAR_WIDTH / 2.,
                            Self::MAX_HEALTH_BAR_WIDTH / 4.,
                            1.,
                        ))
                        // Position the healthbar just above the player
                        .with_local_position(Vector3::new(-0.1, 0.25, 0.01))
                        .build(),
                ),
        )
        // Gray color
        .with_color(Color::opaque(50, 50, 50))
        .build(&mut context.scene.graph);

        // Create the healthbar progress
        let health_bar_progress = RectangleBuilder::new(
            BaseBuilder::new()
                .with_name("HealthBarProgress")
                .with_local_transform(
                    TransformBuilder::new()
                        // Resize the healthbar so it is wide and short
                        .with_local_scale(Vector3::new(
                            Self::MAX_HEALTH_BAR_WIDTH,
                            Self::MAX_HEALTH_BAR_WIDTH / 4.,
                            1.,
                        ))
                        // Move the pivot center to the top left of the healthbar
                        // This is so it can be scaled from the left to the right
                        .with_scaling_pivot(Vector3::new(
                            Self::MAX_HEALTH_BAR_WIDTH / 2.,
                            Self::MAX_HEALTH_BAR_WIDTH / 4.,
                            1.,
                        ))
                        // Position the healthbar just above the player, and just in front of the background
                        .with_local_position(Vector3::new(-0.1, 0.35, 0.0))
                        .build(),
                ),
        )
        .with_color(Color::GREEN)
        .build(&mut context.scene.graph);

        // Make the healthbar background and progress child nodes of the player
        context
            .scene
            .graph
            .link_nodes(health_bar_background, context.handle);
        context
            .scene
            .graph
            .link_nodes(health_bar_progress, context.handle);

        // Set the Player struct properties
        self.max_health = Self::PLAYER_STARTING_HEALTH;
        self.health = Self::PLAYER_STARTING_HEALTH;
        self.health_bar_background = health_bar_background;
        self.health_bar_progress = health_bar_progress;
    }

    pub fn take_damage(&mut self, damage: &f32) {
        if self.health == 0.0 {
            return;
        }
        self.health -= damage;
    }

    pub fn get_health(&self) -> f32 {
        return self.health;
    }

    pub fn loop_over(&mut self, context: &mut ScriptContext) {
        let rigid_body = context.scene.graph[context.handle].cast_mut::<RigidBody>();
        match rigid_body {
            Some(rigid_data) => {
                self.prev_y_velosity = rigid_data.lin_vel().y;
            }
            None => {}
        }
    }

    pub fn key_checker(&mut self, context: &mut ScriptContext, keycode: KeyCode, is_pressed: bool) {
        match keycode {
            KeyCode::ArrowLeft => self.move_left = is_pressed,
            KeyCode::ArrowRight => self.move_right = is_pressed,
            KeyCode::ArrowUp => self.jump = is_pressed,
            KeyCode::Space => {
                if !is_pressed {
                    self.fight = false;
                    self.start_fight_last = 0.0;
                    return;
                }
                if (context.elapsed_time - self.start_fight_last) < 0.1 {
                    self.fight = false;
                    self.start_fight_last = context.elapsed_time;
                    return;
                }
                self.start_fight_last = context.elapsed_time;
                self.fight = is_pressed;
            }
            _ => (),
        }
    }

    fn get_speed(&self) -> f32 {
        if self.health <= 0.0 {
            return 0.0;
        }
        return if self.move_left {
            3.0
        } else if self.move_right {
            -3.0
        } else {
            0.0
        };
    }

    pub fn move_do(&mut self, context: &mut ScriptContext) {
        let rigid_body = context.scene.graph[context.handle].cast_mut::<RigidBody>();
        match rigid_body {
            Some(rigid_data) => {
                let x_speed = self.get_speed();
                rigid_data.set_lin_vel(Vector2::new(x_speed, rigid_data.lin_vel().y));
            }
            None => {}
        }
    }

    pub fn is_in_fly(&self, rigid_data: &RigidBody) -> bool {
        let dif_y = self.prev_y_velosity - rigid_data.lin_vel().y;
        if dif_y > 0.0 && dif_y > 0.1 {
            return true;
        }
        if dif_y < 0.0 && dif_y < -0.1 {
            return true;
        }
        return false;
    }

    pub fn jump_do(&mut self, context: &mut ScriptContext) {
        if self.health <= 0.0 {
            return;
        }
        let rigid_body = context.scene.graph[context.handle].cast_mut::<RigidBody>();
        match rigid_body {
            Some(rigid_data) => {
                if self.jump && !self.is_in_fly(rigid_data) {
                    self.start_y_position = rigid_data.global_position().y;
                    rigid_data.set_lin_vel(Vector2::new(rigid_data.lin_vel().x, 6.0));
                    return;
                }
                if rigid_data.lin_vel().y == 0.0 && !self.is_in_fly(rigid_data) {
                    self.start_y_position = rigid_data.global_position().y;
                }

                // Log::info(format!(
                //     "pos {}, dif_height: {}, lin_vel: {}, jump: {}, is_jumping: {}",
                //     self.prev_y_velosity - rigid_data.lin_vel().y,
                //     self.dif_height(rigid_data),
                //     rigid_data.lin_vel().y,
                //     self.prev_y_velosity - rigid_data.lin_vel().y,
                //     self.is_in_fly(rigid_data),
                // ));
            }
            None => {}
        }
    }

    pub fn change_orientation(&self, context: &mut ScriptContext) {
        let sprite = context.scene.graph.try_get_mut(self.sprite);
        match sprite {
            Some(sprite_data) => {
                let x_speed = self.get_speed();
                if x_speed == 0.0 {
                    return;
                }
                // 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 add_animation_to_stack(&mut self, animation: Animations) {
        if self.current_animation.len() > 2 {
            return;
        }
        let mut last_anim: Option<u32> = None;
        match self.current_animation.front() {
            Some(data) => {
                last_anim = Some(data.clone());
            }
            None => {}
        }
        match last_anim {
            Some(last_anim_data) => {
                if last_anim_data == (animation as u32) {
                    return;
                }
                if animation == Animations::Idle && last_anim_data != Animations::Idle as u32 {
                    return;
                }
                self.current_animation.push_front(animation as u32)
            }
            None => self.current_animation.push_front(animation as u32),
        }
    }

    fn attack_enemy(&self, context: &mut ScriptContext) {
        if !self.fight {
            return;
        }
        let self_node = match context.scene.graph.try_get(context.handle) {
            Some(node) => node,
            None => return,
        };
        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();
        let x_coord: [f32; 2] = [-1.0, 1.0];
        for x in x_coord.iter() {
            context.scene.graph.physics2d.cast_ray(
                RayCastOptions {
                    ray_origin: Point2::new(self_position.x, self_position.y),
                    ray_direction: Vector2::new(x.clone(), 0.0),
                    max_len: 1.0,
                    groups: InteractionGroups::default(),
                    // groups: InteractionGroups::default(),
                    // Sort the results by distance
                    sort_results: true,
                },
                // Store the collisions in the vector
                &mut buffer,
            );
            if buffer.len() == 1 {
                continue;
            }
            break;
        }
        for i in 0..buffer.len() {
            if buffer[i].toi == 0.0 {
                continue;
            }
            if let Some(d) = context.scene.graph.try_get(buffer[i].collider) {
                match context
                    .scene
                    .graph
                    .try_get_script_of_mut::<Enemy>(d.parent())
                {
                    Some(script) => {
                        script.take_damage(&self.damage);
                        break;
                    }
                    None => {}
                }
            }
        }
    }

    fn animate_choose(&mut self, context: &mut ScriptContext) {
        if self.health <= 0.0 {
            return;
        }
        let rigid_body = context.scene.graph[context.handle].cast_mut::<RigidBody>();
        if let Some(rigid_data) = rigid_body {
            if self.jump && (rigid_data.lin_vel().y as i32) < 1 {
                self.add_animation_to_stack(Animations::Idle);
                // self.current_animation.push_front(Animations::Idle as u32) ;
            }
            if self.jump {
                self.add_animation_to_stack(Animations::Jump);
                self.current_animation.push_front(Animations::Jump as u32);
            }
        }

        let x_speed = self.get_speed();
        if self.fight {
            self.add_animation_to_stack(Animations::Fight);
            // self.current_animation.push_front(Animations::Fight as u32);
        }
        if x_speed != 0.0 {
            self.add_animation_to_stack(Animations::Steps);
            // self.current_animation.push_front(Animations::Steps as u32);
        } else {
            self.add_animation_to_stack(Animations::Idle);
            // self.current_animation.push_front(Animations::Idle as u32);
        }
    }

    fn update_health(&mut self, context: &mut ScriptContext) {
        // Grab the healthbar progess node
        let health_bar_progress = context.scene.graph[self.health_bar_progress].as_rectangle_mut();

        // Grab the healthbar progess node's transform
        let health_bar_transform = health_bar_progress.local_transform_mut();

        // Grab the current scale of the healthbar progess node
        let health_bar_scale = health_bar_transform.scale();

        // Calculate the new scale of the healthbar progess node
        // Don't let it go below 0 HP
        let new_health = f32::max(
            (self.health / self.max_health) * Self::MAX_HEALTH_BAR_WIDTH,
            0.0,
        );

        // If the new scale is different from the current scale, update the scale
        if health_bar_scale.x != new_health {
            health_bar_transform.set_scale(Vector3::new(
                new_health,
                // Don't change the y or z scale
                health_bar_scale.y,
                health_bar_scale.z,
            ));
        }
    }

    pub fn animation_do(&mut self, context: &mut ScriptContext) {
        self.animate_choose(context);
        //let can_update_frame = self.is_need_update_anim_frame(context);
        let mut current_animation = Animations::Idle as usize;
        if self.health <= 0.0 {
            current_animation = Animations::Dead as usize;
        }
        if !self.current_animation.is_empty() {
            match self.current_animation.pop_back() {
                Some(anim) => {
                    current_animation = anim as usize;
                }
                None => {}
            }
        }
        let current_animation = self.animations.get_mut(current_animation);
        match current_animation {
            Some(animation_data) => {
                animation_data.update(context.dt);

                if let Some(sprite) = context
                    .scene
                    .graph
                    .try_get_mut(self.sprite)
                    .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_component_provider!(Player,);

impl ScriptTrait for Player {
    // Called once at initialization.
    fn on_init(&mut self, context: &mut ScriptContext) {
        self.init(context);
        self.change_orientation(context);
        self.window_height = 600;
        self.window_height = 800;
    }

    // Put start logic - it is called when every other script is already initialized.
    fn on_start(&mut self, context: &mut ScriptContext) {
        context
            .message_dispatcher
            .subscribe_to::<Message>(context.handle);
    }

    fn on_message(
        &mut self,
        message: &mut dyn ScriptMessagePayload,
        _ctx: &mut ScriptMessageContext,
    ) {
        // React to message.
        if let Some(Message::Resize { width, height }) = message.downcast_ref::<Message>() {
            self.window_height = height.clone();
            self.window_width = width.clone();
        }
    }

    // Called whenever there is an event from OS (mouse click, keypress, etc.)
    fn on_os_event(&mut self, event: &Event<()>, context: &mut ScriptContext) {
        if let Event::WindowEvent { event, .. } = event {
            if let WindowEvent::KeyboardInput { event, .. } = event {
                if let PhysicalKey::Code(keycode) = event.physical_key {
                    let is_pressed = event.state == ElementState::Pressed;
                    self.key_checker(context, keycode, is_pressed);
                }
            }
            if let WindowEvent::Touch(touch) = event {
                match touch.phase {
                    TouchPhase::Started => {
                        Log::info(format!(
                            "touch x {:?}, y {:?}",
                            touch.location.x, touch.location.y
                        ));
                        if touch.location.x > (self.window_width / 2) as f64 {
                            self.key_checker(context, KeyCode::ArrowRight, true);
                            self.key_checker(context, KeyCode::ArrowLeft, false);
                        } else {
                            self.key_checker(context, KeyCode::ArrowLeft, true);
                            self.key_checker(context, KeyCode::ArrowRight, false);
                        }
                        if touch.location.y < (self.window_height / 2) as f64 {
                            self.key_checker(context, KeyCode::ArrowUp, true);
                        }
                        if touch.location.y > (self.window_height / 2) as f64 {
                            self.key_checker(context, KeyCode::ArrowUp, false);
                        }
                    }
                    TouchPhase::Ended => {
                        if touch.location.x > (self.window_width / 2) as f64 {
                            self.key_checker(context, KeyCode::ArrowRight, false);
                        } else {
                            self.key_checker(context, KeyCode::ArrowLeft, false);
                        }
                        if touch.location.y < (self.window_height / 2) as f64 {
                            self.key_checker(context, KeyCode::ArrowUp, false);
                        }
                    }
                    _ => {}
                }
            }
        }
    }

    // Called every frame at fixed rate of 60 FPS.
    fn on_update(&mut self, context: &mut ScriptContext) {
        // The script can be assigned to any scene node, but we assert that it will work only with
        // 2d rigid body nodes.
        self.jump_do(context);
        self.move_do(context);
        self.change_orientation(context);
        self.animation_do(context);
        self.update_health(context);
        self.attack_enemy(context);
        self.loop_over(context);
    }
}
