use engine::prelude::*;
use rpg_components::{
    components::{abilityloader::AbilityLoader, statistics::Statistics},
    damage_type::DamageType,
    items::{ability_addon::AbilityAddonCollection, ability_book::AbilityBook},
};

use crate::game::content::abilities::handle_npc_death;

use anyhow::Result;
use cgmath::{vec3, Deg, Matrix2, Vector2};

use super::{
    super::prelude::*,
    particle_spawn::{ParticleSpawn, ParticleSpawnExecutor},
};

use crate::game::game::GameHandle;
use std::time::Duration;

const ODD_ANGLES: [Deg<f32>; 11] = [
    Deg(0.0),
    Deg(10.0),
    Deg(-10.0),
    Deg(20.0),
    Deg(-20.0),
    Deg(30.0),
    Deg(-30.0),
    Deg(40.0),
    Deg(-40.0),
    Deg(50.0),
    Deg(-50.0),
];

const EVEN_ANGLES: [Deg<f32>; 10] = [
    Deg(5.0),
    Deg(-5.0),
    Deg(15.0),
    Deg(-15.0),
    Deg(25.0),
    Deg(-25.0),
    Deg(35.0),
    Deg(-35.0),
    Deg(45.0),
    Deg(-45.0),
];

pub struct ProjectileMarker;

impl EntityComponent for ProjectileMarker {
    fn name(&self) -> &str {
        Self::debug_name()
    }
}

impl ComponentDebug for ProjectileMarker {
    fn debug_name() -> &'static str {
        "ProjectileMarker"
    }
}

#[derive(Clone)]
pub struct Projectile;

impl Projectile {
    fn hit_event(
        me: Entity,
        collider: Entity,
        owner: Entity,
        level: u32,
        addons: &AbilityAddonCollection,
        game_handle: &GameHandle,
        scene: &mut SceneContents<'_>,
    ) -> Result<CollisionEventType> {
        // this handle is only needed for calculating loot bonus when creating a loot stash
        // it only queries all users and their loot bonus passive
        let s = unsafe { remove_life_time_mut(scene) };

        let game = game_handle.upgrade();

        let mut entity_multi_mut = scene.entities_multi_mut();

        let collider_object = match entity_multi_mut.get(collider) {
            Ok(collider) => collider,
            Err(_) => return Ok(CollisionEventType::Ignore),
        };

        let projectile_object = match entity_multi_mut.get(me) {
            Ok(projectile_object) => projectile_object,
            Err(_) => return Ok(CollisionEventType::Ignore),
        };

        let mut collider_components = collider_object.multi_mut();
        let mut projectile_components = projectile_object.multi_mut();

        let my_faction = Faction::get(&mut projectile_components).unwrap();
        let ability_info = projectile_components.get::<AbilityLoader>()?;

        // don't collide with same 'Team' or neutral NPC's
        let collider_faction = Faction::get(&mut collider_components);

        if !my_faction.check_collision(collider_faction) {
            // if there is no entity faction, that means we hit an environment object
            // e.g. a tree or a stone.
            // we then bounce or destroy the projectile
            if addons.bounce() {
                let position = projectile_components.get::<Location>().unwrap().position();
                let movement = projectile_components.get::<Movement>().unwrap();

                let direction = movement.direction();
                let inverted = -direction;
                let random_angle = Deg(Random::range_f32(-60.0, 60.0));

                let new_direction = Matrix2::from_angle(random_angle) * inverted;

                movement.set_direction(new_direction);

                let location_info = projectile_components.get::<AbilityLocationInfo>()?;

                location_info.new_direction_at(position);
            }

            return Ok(CollisionEventType::Ignore);
        }

        // skip any damage related calculation when the owner isn't present anymore
        if let Ok(owner_object) = entity_multi_mut.get(owner) {
            let owner_stats = owner_object.get_component::<Statistics>()?;

            let mut base_damage = ability_info.damage(level, addons, owner_stats);

            let explosion_radius =
                ability_info.settings.parameter.target_radius + addons.explosion_radius();

            let npc_location = projectile_components.get::<Location>().unwrap();

            // don't cast an explosion when the radius is 0
            if explosion_radius == 0.0 {
                base_damage = AbilityLoader::check_for_crit(base_damage, owner_stats);

                super::damage_and_experience(
                    base_damage,
                    ability_info.settings.parameter.damage_type,
                    &mut collider_components,
                    owner_object.multi_mut(),
                    s,
                )?;

                unsafe {
                    collider_components.clear_all_usages();
                }

                // spawn on hit particle effects
                if !ability_info.settings.parameter.on_hit_particles.is_empty() {
                    let file_name =
                        game.build_data_path(&ability_info.settings.parameter.on_hit_particles);
                    let info = ParticleSystemInfo::load(file_name)?;

                    s.write_event(OnHitParticles {
                        particle_system_info: info,
                        collision_sound: ability_info.settings.parameter.on_collision_sound.clone(),
                        position: npc_location.position(),
                        offset: npc_location.offset(),

                        arc_info: None,
                    });
                }

                unsafe {
                    collider_components.clear_all_usages();
                }

                handle_npc_death(&game, s, collider, collider_components)?;
            }
            // if there is a explosion radius, create an explosion according to its radius
            else {
                let file_name = match ability_info.settings.parameter.damage_type {
                    DamageType::Physical => todo!(),
                    DamageType::Fire => game.build_data_path("particles/fire_explosion.particle"),
                    DamageType::Water => game.build_data_path("particles/ice_explosion.particle"),
                    DamageType::Air => todo!(),
                };

                let mut info = ParticleSystemInfo::load(file_name)?;
                info.particles.minimum_velocity =
                    explosion_radius / info.system.duration.as_secs_f32();
                info.particles.maximum_velocity = info.particles.minimum_velocity;

                // spawn on hit particle effects
                s.write_event(OnHitParticles {
                    particle_system_info: info,
                    collision_sound: ability_info.settings.parameter.on_collision_sound.clone(),
                    position: npc_location.position(),
                    offset: npc_location.offset(),

                    arc_info: Some(ArcInfo {
                        radius: explosion_radius,
                        base_damage,
                        damage_type: ability_info.settings.parameter.damage_type,
                        owner,
                    }),
                });
            }
        }

        scene.remove_entity(me)?;

        Ok(CollisionEventType::Ignore)
    }

    pub fn create_particles(
        info: &AbilityLoader,
        game_handle: &GameHandle,
        draw: &mut Draw,
        particle_system_vulkan_objects: &ParticleSystemVulkanObjects,
    ) -> Result<ParticleSystem> {
        let game = game_handle.upgrade();
        let file_name = game.build_data_path(&info.settings.parameter.trail);

        let mut particle_info = ParticleSystemInfo::load(file_name)?;
        particle_info.system.duration = Duration::from_secs(300).into();
        particle_info.particles.minimum_velocity = info.settings.parameter.speed / 3.0;
        particle_info.particles.maximum_velocity = info.settings.parameter.speed / 2.0;

        ParticleSystem::new_with_vk_objects(
            particle_info,
            game.engine(),
            particle_system_vulkan_objects,
            draw,
        )
    }
}

impl Projectile {
    pub fn execute(
        ability: &AbilityLoader,
        _owner: Entity,
        owner_components: &mut MultiMut<'_>,
        direction: Vector2<f32>,
        book: &AbilityBook,
        game_handle: &GameHandle,
        entities: &mut Entities<'_>,
        events: &mut ContentEvents<'_>,
    ) -> Result<()> {
        let mut projectile_count = 1 + book.addons().additional_projectiles() as usize;

        let angles: &[Deg<f32>] = if (projectile_count % 2) == 0 {
            projectile_count = projectile_count.min(EVEN_ANGLES.len());

            &EVEN_ANGLES
        } else {
            projectile_count = projectile_count.min(ODD_ANGLES.len());

            &ODD_ANGLES
        };

        let owner_hitbox = owner_components.get::<HitBox>()?;
        let owner_location = owner_components.get::<Location>()?;

        let projectile_size = book.addons().size();
        let projectile_speed = book.addons().projectile_speed();
        let projectile_distance = ability.settings.parameter.distance + book.addons().distance();

        let ability_data = book.ability().data();

        let game = game_handle.upgrade();

        (0..projectile_count)
            .into_iter()
            .try_for_each(|i| -> Result<()> {
                let angle = &angles[i];

                let mut projectile_entity = game
                    .entity_manager()
                    .load_entity(game.engine().assets(), &ability.settings.meta.entity)?;

                {
                    // alter the hitbox parameters
                    let projectile_hitbox = projectile_entity.get_component_mut::<HitBox>()?;

                    // Multiply the radius with potential ability modifications
                    projectile_hitbox.set_radius(projectile_size * projectile_hitbox.radius());

                    // set on hit event for the hitbox (only the server side will check for it)

                    projectile_hitbox.set_event({
                        let owner = _owner;
                        let level = book.level();
                        let addons = book.addons().clone();
                        let game_handle = game_handle.clone();

                        move |scene, me, collider| {
                            Self::hit_event(
                                me,
                                collider,
                                owner,
                                level,
                                &addons,
                                &game_handle,
                                scene,
                            )
                        }
                    });

                    let hitbox_radius = projectile_hitbox.radius();

                    let projectile_location = Location::new_and_setup(&mut projectile_entity)?;

                    // set projectile to owner location and set rotation accordingly
                    projectile_location.set_scale_uniform(projectile_size);

                    let direction = Matrix2::from_angle(*angle) * direction;
                    let position = owner_location.position()
                        + direction.extend(0.0) * (hitbox_radius * 1.1 + owner_hitbox.radius());

                    projectile_location.update(position, direction);

                    // apply height offset
                    projectile_location.set_offset(vec3(
                        0.0,
                        0.0,
                        ability_data.settings.parameter.height_offset,
                    ));

                    let mut projectile_movement = Movement::new(
                        ability.settings.parameter.speed + projectile_speed,
                        projectile_entity.get_component_mut::<Audio>().ok(),
                    );

                    if !ability_data.settings.parameter.idle_sound.is_empty() {
                        let audio = projectile_entity.get_component_mut::<Audio>().unwrap();

                        audio.add_custom_sound(
                            game.build_data_path(&ability_data.settings.parameter.idle_sound),
                            "idle_sound",
                        )?;
                    };

                    // set movement
                    projectile_movement.set_direction(direction);

                    let ability_location_info = AbilityLocationInfo::new(
                        position,
                        projectile_distance,
                        projectile_entity.get_component_mut::<Audio>().unwrap(),
                    );

                    if !ability_data.settings.parameter.trail.is_empty() {
                        events.write_event(ParticleSpawn {
                            entity: projectile_entity.as_entity(),
                            info: ability_data.clone(),
                            executor: ParticleSpawnExecutor::Projectile,
                        });
                    }

                    projectile_entity.insert_component(ability_location_info);
                    projectile_entity.insert_component(ability.clone());
                    projectile_entity.insert_component(projectile_movement);
                    projectile_entity.insert_component(ProjectileMarker);
                    Faction::copy(owner_components, &mut projectile_entity);
                }

                entities.add_entity(projectile_entity)?;

                Ok(())
            })?;

        Ok(())
    }
}