use glc::{
    math::{linear_step, sqr},
    matrix::Matrix3f,
    vector::{Vector2f, Vector3f},
};
use rand::random;
use specs::{prelude::*, ParJoin, Read, ReadStorage, System, WriteStorage};

use crate::{
    components::{Fleet, FleetOwned, Position, Ship, Velocity},
    constants::{
        SHIP_ORBIT_AGILITY, SHIP_ORBIT_ANGLE_DELTA_MIN, SHIP_ORBIT_ANGLE_DELTA_RND,
        SHIP_ORBIT_DISTANCE_MAX, VECTOR_2F_POS_X,
    },
    resources::Global,
};

#[derive(Default)]
pub struct Fleets;

#[derive(SystemData)]
pub struct FleetsData<'a> {
    ship: WriteStorage<'a, Ship>,
    velocity: WriteStorage<'a, Velocity>,
    fleet_owned: ReadStorage<'a, FleetOwned>,
    position: ReadStorage<'a, Position>,
    fleet: ReadStorage<'a, Fleet>,
    global: Read<'a, Global>,
}

impl<'a> System<'a> for Fleets {
    type SystemData = FleetsData<'a>;

    fn run(&mut self, data: Self::SystemData) {
        let FleetsData {
            mut ship,
            mut velocity,
            fleet_owned,
            position,
            fleet,
            global,
        } = data;

        (&mut ship, &mut velocity, &fleet_owned, &position)
            .par_join()
            .for_each(|(ship, vel, fleet_owned, pos)| {
                progress_ship(
                    &position,
                    &fleet,
                    ship,
                    vel,
                    fleet_owned.owner,
                    pos,
                    global.delta,
                )
            });
    }
}

fn progress_ship<'a>(
    positions: &ReadStorage<'a, Position>,
    fleets: &ReadStorage<'a, Fleet>,
    ship: &mut Ship,
    velocity: &mut Velocity,
    fleet_id: Entity,
    position: &Position,
    delta: f32,
) {
    let (orbit_pos, orbit_min, orbit_max): (Vector2f, f32, f32) =
        match (positions.get(fleet_id), fleets.get(fleet_id)) {
            (Some(position), Some(fleet)) => (position.pos, fleet.orbit_min, fleet.orbit_max),
            (_, _) => return,
        };

    let orbit_to_target = ship.target_pos - orbit_pos;
    let orbit_to_ship = position.pos - orbit_pos;
    let ship_to_target = ship.target_pos - position.pos;

    let target_radius = orbit_to_target.length_sqr();
    let ship_radius = orbit_to_ship.length_sqr();

    let target_in_orbit = (target_radius <= sqr(orbit_max)) && (target_radius >= sqr(orbit_min));
    let ship_in_orbit = ship_radius < sqr(SHIP_ORBIT_DISTANCE_MAX * orbit_max);

    /* check and update target posistion */
    if ship.target_dir.length_sqr() == 0.0
        || ship_in_orbit != target_in_orbit
        || ship.target_dir * ship_to_target <= 0.0
    {
        if ship_in_orbit && orbit_max > 0.0 {
            let orbit_to_ship_vec3 = Vector3f::new(orbit_to_ship.x, orbit_to_ship.y, 0.0);
            let ship_dir_vec3 = Vector3f::new(velocity.dir.x, velocity.dir.y, 0.0);

            let dir = if orbit_to_ship_vec3.cross(&ship_dir_vec3).z > 0.0 {
                1.0
            } else {
                -1.0
            };

            let angle = orbit_to_ship.angle2(&VECTOR_2F_POS_X);
            let angle = angle
                + (SHIP_ORBIT_ANGLE_DELTA_MIN + SHIP_ORBIT_ANGLE_DELTA_RND * random()) * dir
                    / orbit_max;
            let radius = orbit_min + (orbit_max - orbit_min) * random::<f32>();

            ship.target_pos.x = orbit_pos.x + radius * angle.cos();
            ship.target_pos.y = orbit_pos.y + radius * angle.sin();
        } else {
            ship.target_pos = orbit_pos;
        }
        ship.target_dir = (ship.target_pos - position.pos).normalize();
    }

    /* update ship direction */
    let angle = ship_to_target.angle2(&velocity.dir);
    if angle.into_inner().abs() > 0.0001 {
        let dir = angle.into_inner() / angle.abs().into_inner();
        let agility = if ship_in_orbit {
            SHIP_ORBIT_AGILITY
        } else {
            ship.agility
        };
        let rot_speed = agility * linear_step(0.0, 45.0, angle.abs().into_deg().into_inner());

        let m = Matrix3f::new(
            Vector3f::new(velocity.dir.y, -velocity.dir.x, 0.0),
            Vector3f::new(velocity.dir.x, velocity.dir.y, 0.0),
            Vector3f::new(0.0, 0.0, 1.0),
        );
        let m = m * Matrix3f::rotate(rot_speed * -dir * delta);

        velocity.dir = Vector2f::new(m.axis_y.x, m.axis_y.y);
    }
}
