#![warn(
     clippy::all,
     clippy::restriction,
     clippy::pedantic,
     clippy::nursery,
     clippy::cargo,
 )]

use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
use bevy::{
    prelude::*, render::camera::Camera, render::camera::OrthographicProjection,
    render::pass::ClearColor,
};
use bevy_egui::{egui, EguiContext, EguiPlugin};
use bevy_rapier2d::prelude::*;
use nalgebra::{Point2, Vector2};
mod util;
use crate::util::*;
mod particle;
use crate::particle::*;



fn main() {
    #[cfg(target_arch = "wasm32")]
    console_error_panic_hook::set_once();

    let mut app = App::build();
    app.insert_resource(WindowDescriptor {
        title: "Bevy game".to_string(),
        vsync: false,
        //mode: bevy::window::WindowMode::BorderlessFullscreen,
        ..Default::default()
    })
    .insert_resource(RapierConfiguration {
        gravity: Vector2::zeros(),
        timestep_mode: bevy_rapier2d::physics::TimestepMode::FixedTimestep, //physic run at fixed 60Hz
        ..Default::default()
    })
    .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
    .add_plugins(DefaultPlugins);

    #[cfg(target_arch = "wasm32")]
    app.add_plugin(bevy_webgl2::WebGL2Plugin);

    //app.add_plugin(LogDiagnosticsPlugin::default())
    //.add_plugin(FrameTimeDiagnosticsPlugin::default())
    app.add_plugin(RapierPhysicsPlugin::<NoUserData>::default())
        .add_plugin(EguiPlugin)
        .init_resource::<PlayerShipMesh>()
        .init_resource::<BulletMesh>()
        .init_resource::<BulletMat>()
        .init_resource::<UiState>()
        .add_startup_system(setup_player_ship.system().before("lel1").before("lel2"))
        .add_startup_system(setup_graphics.system().label("lel1"))
        .add_startup_system(setup_physics.system().label("lel2"))
        .add_system(hehe.system())
        .add_system(manipulate_rigidbody.system().label("move_playa"))
        .add_plugin(ParticlePlugin)
        //.add_system(add_air_angular_friction.system())
        .add_system(shoot.system())
        .add_system_to_stage(CoreStage::PostUpdate, camera_fix.system())
        .add_system(change_scale.system())
        //.add_system(camera_fix.system().after("rapier::transform_sync_stage"))
        .run();
}

// Note the usage of `ResMut`. Even though `ctx` method doesn't require
// mutability, accessing the context from different threads will result
// into panic if you don't enable `egui/multi_threaded` feature.
//fn ui_example(egui_context: ResMut<EguiContext>) {
//    egui::Window::new("Hello").show(egui_context.ctx(), |ui| {
//        ui.label("world");
//    });
//}

struct UiState {
    label: String,
    value: f32,
}

impl Default for UiState {
    fn default() -> Self {
        UiState {
            label: "game".to_string(),
            value: 15.0,
        }
    }
}

#[derive(Default)]
struct PlayerShipMesh {
    mesh: Handle<Mesh>,
    texture: Handle<Texture>,
}
#[derive(Default)]
struct BulletMesh(Handle<Mesh>);

#[derive(Default)]
struct BulletMat(Handle<StandardMaterial>);

struct PlayerShip {
    last_shot: f64,
}

fn setup_player_ship(
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut playermesh: ResMut<PlayerShipMesh>,
    mut bulletmesh: ResMut<BulletMesh>,
    mut bulletmat: ResMut<BulletMat>,
    server: Res<AssetServer>,
) {
    let mut ship = Mesh::new(bevy::render::pipeline::PrimitiveTopology::TriangleList);
    let v_pos = vec![
        [0.5, 3.0, 0.0],
        [0.0, 2.0, 0.0],
        [1.0, 2.0, 0.0],
        [0.0, 0.0, 0.0],
        [1.0, 0.0, 0.0],
    ];
    ship.set_attribute(Mesh::ATTRIBUTE_POSITION, v_pos);

    let mut v_normal = vec![[0.0, 0.0, 1.0]];
    v_normal.extend_from_slice(&[[0.0, 0.0, 1.0]; 4]);
    ship.set_attribute("Vertex_Normal", v_normal);

    let uv = vec![[0.5, 0.5], [0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
    ship.set_attribute("Vertex_Uv", uv);

    let indices = vec![0, 1, 2, 1, 3, 2, 2, 3, 4];

    ship.set_indices(Some(bevy::render::mesh::Indices::U32(indices)));

    playermesh.mesh = meshes.add(ship);
    playermesh.texture = server.load("ship.png");

    bulletmesh.0 = meshes.add(Mesh::from(bevy::prelude::shape::Icosphere {
        radius: 0.25,
        subdivisions: 2,
    }));

    let bullet_mat = bevy::prelude::StandardMaterial {
        base_color: Color::rgb(1.0, 1.0, 0.0),
        emissive: Color::rgb(1.0, 1.0, 0.0),
        ..Default::default()
    };
    bulletmat.0 = materials.add(bullet_mat);
}

fn setup_graphics(mut commands: Commands, mut configuration: ResMut<RapierConfiguration>) {
    configuration.scale = 1.0;

    let mut camera = OrthographicCameraBundle::new_3d();
    camera.orthographic_projection.scale = 20.0;
    camera.transform = Transform::from_xyz(0.01, 0.01, 5.01).looking_at(Vec3::ZERO, Vec3::Y);

    let cam = commands.spawn_bundle(camera).id();

    let light = commands
        .spawn_bundle(LightBundle {
            transform: Transform::from_translation(Vec3::new(0.0, 0.0, 2000.0)),
            light: Light {
                intensity: 100_000_00.0,
                range: 6000.0,
                ..Default::default()
            },
            ..Default::default()
        })
        .id();

    commands.entity(cam).push_children(&[light]);
}

fn setup_physics(
    mut commands: Commands,
    playermesh: Res<PlayerShipMesh>,
    particle_m: Res<ParticleMaterial>,
    mut materials2: ResMut<Assets<StandardMaterial>>,
    mut meshes: ResMut<Assets<Mesh>>,
    time: Res<Time>,
) {
    /* Create the ground. */
    /*let collider = ColliderBundle {
        shape: ColliderShape::cuboid(10.0, 10.0),
        ..Default::default()
    };*/

    /* Create the random mesh! */

    //commands.spawn_bundle(collider).insert(ColliderDebugRender::default()).insert(ColliderPositionSync::Discrete);

    let asteroid_mesh = util::generate_random_mesh2(1024, 0.5, 15.0).unwrap().0;
    let asteroid_collider = util::generate_shape_collider_from_mesh(&asteroid_mesh).unwrap();

    let asteroid_rigid_body = RigidBodyBundle {
        position: Vec2::new(20.0, 0.0).into(),
        forces: RigidBodyForces {
            gravity_scale: 0.0,
            ..Default::default()
        },
        ..Default::default()
    };

    let asteroid_pbr = PbrBundle {
        mesh: meshes.add(asteroid_mesh),
        material: materials2.add(bevy::prelude::StandardMaterial {
            base_color_texture: Some(playermesh.texture.clone()),
            roughness: 1.0,
            ..Default::default()
        }),
        ..Default::default()
    };

    let asteroid_collider = ColliderBundle {
        shape: asteroid_collider,
        material: ColliderMaterial {
            restitution: 0.7,
            ..Default::default()
        },
        mass_properties: ColliderMassProps::Density(200.0),
        ..Default::default()
    };

    commands
        .spawn_bundle(asteroid_rigid_body)
        .insert_bundle(asteroid_collider)
        .insert_bundle(asteroid_pbr)
        .insert(ColliderPositionSync::Discrete);

    let asteroid_mesh = util::generate_random_mesh2(256, 0.5, 15.0).unwrap().0;
    let asteroid_collider = util::generate_shape_collider_from_mesh(&asteroid_mesh).unwrap();

    let asteroid_rigid_body = RigidBodyBundle {
        position: Vec2::new(0.0, 10.0).into(),
        forces: RigidBodyForces {
            gravity_scale: 0.0,
            ..Default::default()
        },
        ..Default::default()
    };

    let asteroid_pbr = PbrBundle {
        mesh: meshes.add(asteroid_mesh),
        material: materials2.add(bevy::prelude::StandardMaterial {
            base_color_texture: Some(playermesh.texture.clone()),
            roughness: 1.0,
            ..Default::default()
        }),
        ..Default::default()
    };

    let asteroid_collider = ColliderBundle {
        shape: asteroid_collider,
        material: ColliderMaterial {
            restitution: 0.7,
            ..Default::default()
        },
        mass_properties: ColliderMassProps::Density(200.0),
        ..Default::default()
    };

    commands
        .spawn_bundle(asteroid_rigid_body)
        .insert_bundle(asteroid_collider)
        .insert_bundle(asteroid_pbr)
        .insert(ColliderPositionSync::Discrete);

    let asteroid_mesh = util::generate_random_mesh2(128, 0.5, 15.0).unwrap().0;
    let asteroid_collider = util::generate_shape_collider_from_mesh(&asteroid_mesh).unwrap();

    let asteroid_rigid_body = RigidBodyBundle {
        position: Vec2::new(-7.0, -7.0).into(),
        forces: RigidBodyForces {
            gravity_scale: 0.0,
            ..Default::default()
        },
        ..Default::default()
    };

    let asteroid_pbr = PbrBundle {
        mesh: meshes.add(asteroid_mesh),
        material: materials2.add(bevy::prelude::StandardMaterial {
            base_color_texture: Some(playermesh.texture.clone()),
            roughness: 1.0,
            ..Default::default()
        }),
        ..Default::default()
    };

    let asteroid_collider = ColliderBundle {
        shape: asteroid_collider,
        material: ColliderMaterial {
            restitution: 0.7,
            ..Default::default()
        },
        mass_properties: ColliderMassProps::Density(200.0),
        ..Default::default()
    };

    commands
        .spawn_bundle(asteroid_rigid_body)
        .insert_bundle(asteroid_collider)
        .insert_bundle(asteroid_pbr)
        .insert(ColliderPositionSync::Discrete);

    let rigid_body = RigidBodyBundle {
        position: Vec2::new(0.0, 0.0).into(),
        forces: RigidBodyForces {
            gravity_scale: 0.0,
            ..Default::default()
        },
        ..Default::default()
    };

    let mesh = meshes.get(playermesh.mesh.clone()).unwrap();

    let ShipCollider = ColliderBundle {
        shape: util::generate_shape_collider_from_mesh(&mesh).unwrap(),
        material: ColliderMaterial {
            restitution: 0.7,
            ..Default::default()
        },
        mass_properties: ColliderMassProps::Density(200.0),
        ..Default::default()
    };

    // set up the camera

    // camera
    //commands.spawn_bundle(camera);

    commands
        .spawn_bundle(rigid_body)
        .insert_bundle(ShipCollider)
        .insert_bundle(PbrBundle {
            mesh: playermesh.mesh.clone(),
            material: materials2.add(bevy::prelude::StandardMaterial {
                base_color_texture: Some(playermesh.texture.clone()),
                roughness: 1.0,
                ..Default::default()
            }),
            ..Default::default()
        })
        .insert(ColliderPositionSync::Discrete)
        .insert(PlayerShip { last_shot: 0.0 });

    commands
        .spawn_bundle(SpriteBundle {
            material: particle_m.0.clone(),
            transform: Transform::from_scale(Vec3::new(0.02, 0.02, 0.1)),
            ..Default::default()
        })
        .insert(Particle { lifetime: 10.0 })
        .insert(ParticleLinearSizeOverTime { change: 0.01 });
}

fn hehe(
    time: Res<Time>,
    positions: Query<(&RigidBodyPosition, &RigidBodyVelocity), With<PlayerShip>>,
    egui_context: ResMut<EguiContext>,
    mut ui_state: ResMut<UiState>,
) {
    egui::Window::new("Hello").show(egui_context.ctx(), |ui| {
        let e = positions.iter().next().unwrap();
        ui.label(format!(
            "world {:.2}: time {:.2} : vel: {:.?} : pos {:.2}",
            e.1.angvel,
            time.seconds_since_startup(),
            e.1.linvel.norm(),
            e.0.position
        ));
        ui.add(egui::Slider::new(&mut ui_state.value, 10.0..=30.0).text("zoom"));
    });
}

fn manipulate_rigidbody(
    keyboard_input: Res<Input<KeyCode>>,
    q_camera: Query<(&Transform, &OrthographicProjection), With<Camera>>,
    mut bodies: Query<
        (
            &mut RigidBodyForces,
            &mut RigidBodyVelocity,
            &RigidBodyMassProps,
            &RigidBodyPosition,
        ),
        With<PlayerShip>,
    >,
    windows: Res<Windows>,
    egui_context: Res<EguiContext>,
    time: Res<Time>,
) {
    let multipler = 600.0 * time.delta_seconds();
    let (mut body_forces, mut body_vel, body_mass_prop, body_pos) = bodies.single_mut().unwrap();
    if keyboard_input.pressed(KeyCode::W) {
        let forc = Vec2::new(0.0, 8.0 * multipler);
        body_vel.apply_impulse(
            body_mass_prop,
            body_pos.position.transform_vector(&forc.into()),
        );
    }
    if keyboard_input.pressed(KeyCode::S) {
        let forc = Vec2::new(0.0, -2.0 * multipler);
        body_vel.apply_impulse(
            body_mass_prop,
            body_pos.position.transform_vector(&forc.into()),
        );
    }
    if keyboard_input.pressed(KeyCode::D) {
        let forc = Vec2::new(6.0 * multipler, 0.0);
        body_vel.apply_impulse(
            body_mass_prop,
            body_pos.position.transform_vector(&forc.into()),
        );
    }
    if keyboard_input.pressed(KeyCode::A) {
        let forc = Vec2::new(-6.0 * multipler, 0.0);
        body_vel.apply_impulse(
            body_mass_prop,
            body_pos.position.transform_vector(&forc.into()),
        );
    }
    if body_vel.linvel.norm() > 10.0 {
        body_vel.linvel.try_set_magnitude(10.0, 5.0);
    }

    let window = windows.get_primary().unwrap();
    let ang_vel = body_vel.angvel;
    // check if the cursor is in the primary window
    if !egui_context.ctx().wants_pointer_input() {
        if let Some(pos) = window.cursor_position() {
            // assuming there is exactly one main camera entity, so this is OK
            let (camera_transform, ortoprojection) = q_camera.single().unwrap();

            // get the size of the window
            //let size = Vec2::new(window.width() as f32, window.height() as f32)/2.0 - Vec2::new(window.width() as f32, window.height() as f32) * ortoprojection.scale / 1000.0;

            let size = Vec2::new(window.width() as f32, window.height() as f32);

            // the default orthographic projection is in pixels from the center;
            // just undo the translation
            //let p = (pos - size/2.0)*ortoprojection.scale / 362.55;
            let current = body_pos.position.transform_vector(&Vector2::new(0.0, 1.0));
            let p = (pos - size / 2.0).normalize();

            let angle = nalgebra::geometry::UnitComplex::rotation_between(
                &current,
                &Vector2::new(p.x, p.y),
            )
            .angle();

            // apply the camera transform
            //let pos_wld = camera_transform.compute_matrix() * p.extend(0.0).extend(1.0);
            //eprintln!("World coords: {}/{}", pos_wld.x, pos_wld.y);
            //eprintln!("mouse: {}/{} ship: {}/{} angle: {}", p.x, p.y, current.x, current.y, angle);

            let mut thrusters_power = 16.0;
            let tanh_constant = 10.0;

            let desired_angle_vel_fn = |angle: f32| {
                angle.signum()
                    * (2.0
                        * thrusters_power
                        * (1.0 / tanh_constant)
                        * (((angle * tanh_constant).abs()).cosh()).ln())
                    .sqrt()
            };

            let mut desired_angle_vel =
                desired_angle_vel_fn(angle - ang_vel * time.delta_seconds());

            // constant torque, exept for angle small enough, then became linear because of tanh. Desired angle velocity is just velocity at angle which should be obtained by this torque from zero angular velocity and zero angle.
            // https://physics.stackexchange.com/questions/320752/how-do-i-get-the-position-at-time-t-if-acceleration-depends-on-position
            let mut torque = thrusters_power * (angle * tanh_constant).tanh().abs();

            println!("{:.5} {:.5}", desired_angle_vel, ang_vel);

            torque *= body_mass_prop.mass();

            if angle < 0.0 {
                if ang_vel > desired_angle_vel {
                    body_forces.torque = -torque;
                } else {
                    body_forces.torque = torque;
                }
            } else {
                if ang_vel < desired_angle_vel {
                    body_forces.torque = torque;
                } else {
                    body_forces.torque = -torque;
                }
            }

            /*if ang_vel < angle.abs() {
                body_vel.apply_torque_impulse(body_mass_prop, 4.0);
            }

            if ang_vel > angle.abs() {
                body_vel.apply_torque_impulse(body_mass_prop, -4.0);
            }*/
        }
    }
    /*if let Some(position) = window.cursor_position() {
        let logical_position = (position / bevy::math::Vec2::new( window.physical_width() as f32, window.physical_height() as f32)) - bevy::math::Vec2::new(0.5, 0.5);
        let desired_vec = crate::nalgebra::Isometry2::from_parts( crate::nalgebra::Translation2::identity(), body_pos.position.rotation ).transform_vector(&Vector2::new(0.0, 1.0));
        //let desired_rot = UnitComplex::rotation_between(&desired_vec, &logical_position);
        println!("{} {}", logical_position, desired_vec);
    } else {
        // cursor is not inside the window
    }*/

    if keyboard_input.pressed(KeyCode::E) {
        if ang_vel < 5.0 {
            body_vel.apply_torque_impulse(body_mass_prop, 4.0);
        }
    } else if keyboard_input.pressed(KeyCode::Q) {
        if ang_vel > -5.0 {
            body_vel.apply_torque_impulse(body_mass_prop, -4.0);
        }
    }
    //damping
    /*else {
        body_vel.apply_torque_impulse(body_mass_prop,  -(ang_vel*4.0));
        //body_vel.angvel = 0.0;
    }*/
}

fn shoot(
    mut commands: Commands,
    keyboard_input: Res<Input<KeyCode>>,
    buttons: Res<Input<MouseButton>>,
    mut player_ship: Query<(&RigidBodyVelocity, &RigidBodyPosition, &mut PlayerShip)>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    time: Res<Time>,
    egui_context: Res<EguiContext>,
    bulletmesh: Res<BulletMesh>,
    bulletmat: Res<BulletMat>,
    particle_m: Res<ParticleMaterial>,
) {
    if keyboard_input.pressed(KeyCode::Space)
        || (buttons.pressed(MouseButton::Left) && !egui_context.ctx().wants_pointer_input())
    {
        let (velocity, position, mut player_ships) =
            player_ship.single_mut().expect("Multiple players!");

        if time.seconds_since_startup() - player_ships.last_shot > 0.25 {
            player_ships.last_shot = time.seconds_since_startup();

            let bullet_pos = position.position * Point2::new(0.5, 3.5);

            let rigid_body = RigidBodyBundle {
                position: bullet_pos.into(),
                forces: RigidBodyForces {
                    gravity_scale: 0.0,
                    ..Default::default()
                },
                velocity: RigidBodyVelocity {
                    linvel: (velocity.linvel + position.position * Vector2::new(0.0, 50.0)).into(),
                    angvel: 0.0,
                },
                ..Default::default()
            };

            let bullet_collider = ColliderBundle {
                shape: ColliderShape::ball(0.5),
                material: ColliderMaterial {
                    restitution: 0.7,
                    ..Default::default()
                },
                mass_properties: ColliderMassProps::Density(2.0),
                ..Default::default()
            };

            let bullet_mat = bevy::prelude::StandardMaterial {
                base_color: Color::rgb(1.0, 1.0, 0.0),
                emissive: Color::rgb(1.0, 1.0, 0.0),
                ..Default::default()
            };

            let pbr_bundle = PbrBundle {
                mesh: bulletmesh.0.clone_weak(),
                //material: materials.add(Color::rgb(1.0, 1.0, 0.0).into()),
                transform: bevy::prelude::Transform::from_translation(Vec3::new(
                    bullet_pos.x,
                    bullet_pos.y,
                    0.0,
                )),
                material: bulletmat.0.clone_weak(),
                ..Default::default()
            };

            commands
                .spawn_bundle(rigid_body)
                .insert_bundle(pbr_bundle)
                .insert_bundle(bullet_collider)
                .insert(ColliderPositionSync::Discrete);
        }
    }
}

fn change_scale(mut cam: Query<&mut OrthographicProjection>, ui_state: Res<UiState>) {
    let mut cam = cam.single_mut().unwrap();
    cam.scale = ui_state.value;
}

fn camera_fix(
    mut query: QuerySet<(
        Query<&mut Transform, With<Camera>>,
        Query<(&Transform, &RigidBodyMassProps), With<PlayerShip>>,
    )>,
) {
    /*let queryy = query.q1().single().expect("wuwu");
    let offset = queryy.1.local_mprops.local_com.clone();
    let offset = Vec3::new(offset.x, offset.y, 0.0);
    let new_pos = *queryy.0*offset;
    let mut c = query.q0_mut().single_mut().expect("hehe");
    c.translation = Vec3::new(new_pos[0], new_pos[1], 0.0) + Vec3::new(0.0, 0.0, 5.01);*/

    let (playership_transform, playership_rigidbody) =
        query.q1().single().expect("Playership not found");

    let offset = playership_rigidbody.local_mprops.local_com.clone();
    let offset = Vec3::new(offset.x, offset.y, 0.0);

    let new_pos = *playership_transform * offset;

    let mut cam_transform = query.q0_mut().single_mut().expect("Camera not found");

    cam_transform.translation = Vec3::new(new_pos[0], new_pos[1], 0.0) + Vec3::new(0.0, 0.0, 5.01);
}