use crate::{consts, core::event_bus::EventBus};
use godot::{
    classes::{AnimatedSprite2D, Sprite2D},
    prelude::*,
};

#[derive(GodotClass)]
#[class(base=Node)]
pub struct LiftSyncController {
    #[export]
    player_anim: Option<Gd<AnimatedSprite2D>>,
    #[export]
    barbell: Option<Gd<Sprite2D>>,

    #[export]
    animation_name: GString,
    #[export]
    bar_positions: Array<Vector2>,
    #[export]
    simulate_reps: bool,
    #[export]
    rep_speed: f32,

    current_rep_progress: f32,
    is_lifting: bool,

    event_bus: Option<Gd<EventBus>>,
    base: Base<Node>,
}

#[godot_api]
impl INode for LiftSyncController {
    fn init(base: Base<Node>) -> Self {
        Self {
            player_anim: None,
            barbell: None,
            animation_name: "lift".into(),
            bar_positions: Array::new(),
            simulate_reps: true,
            rep_speed: 2.0,
            current_rep_progress: 0.0,
            is_lifting: false,
            event_bus: None,
            base,
        }
    }

    fn ready(&mut self) {
        let mut bus = self.base().get_node_as::<EventBus>(consts::EVENT_BUS_PATH);
        bus.connect(
            consts::LIFT_VISUAL_HEIGHT,
            &self.base().callable("handle_direct_sync"),
        );
        bus.connect(
            consts::LIFT_EFFORT_APPLIED,
            &self.base().callable("handle_lift_effort"),
        );
        bus.connect(
            consts::FOCUS_RELEASED,
            &self.base().callable("on_focus_released"),
        );

        self.event_bus = Some(bus);
    }

    fn process(&mut self, delta: f64) {
        if !self.simulate_reps {
            return;
        }

        let dt = delta as f32;

        if self.is_lifting {
            self.current_rep_progress += self.rep_speed * dt;
        } else {
            if self.current_rep_progress > 0.0 {
                self.current_rep_progress -= self.rep_speed * dt;
                self.current_rep_progress = self.current_rep_progress.max(0.0);
            }
        }

        let ping_pong_value = godot::global::pingpong(self.current_rep_progress as f64, 1.0) as f32;
        self.update_visuals(ping_pong_value);
        self.is_lifting = false;
    }
}

#[godot_api]
impl LiftSyncController {
    fn update_visuals(&mut self, normalized_height: f32) {
        if self.player_anim.is_none() || self.barbell.is_none() || self.bar_positions.is_empty() {
            godot_error!("LiftSyncController: Missing visuals or bar positions");
            return;
        }

        let total_frames = self.bar_positions.len() as i32;
        let frame_index_f = normalized_height * (total_frames as f32 - 0.01);
        let frame_index = (frame_index_f.floor() as i32).clamp(0, total_frames - 1);

        if let Some(anim) = &mut self.player_anim {
            anim.set_frame(frame_index);

            let current_anim = anim.get_animation();
            if current_anim != StringName::from(&self.animation_name) {
                let anim_name_sn = StringName::from(&self.animation_name);
                anim.play_ex().name(&anim_name_sn).done();
            }
        }

        if let Some(bar) = &mut self.barbell {
            if let Some(pos) = self.bar_positions.get(frame_index as usize) {
                bar.set_position(pos);
            }
        }
    }

    #[func]
    fn handle_direct_sync(&mut self, height: f32) {
        if !self.simulate_reps {
            self.update_visuals(height);
        }
    }

    #[func]
    fn handle_lift_effort(&mut self, _delta: f32) {
        self.is_lifting = true;
    }

    #[func]
    fn on_focus_released(&mut self) {
        self.is_lifting = false;
    }
}
