use crate::ecs::{EditorWorld, GrabAxis, GrabConstraint, GrabEntry, GrabSession};
use crate::systems::input;
use nightshade::ecs::input::resources::MouseState;
use nightshade::ecs::transform::commands::mark_local_transform_dirty;
use nightshade::ecs::window::resources::ViewportRect;
use nightshade::prelude::*;
use nightshade::render::wgpu::passes::geometry::{UiLayer, UiRect};

const AXIS_LINE_LENGTH_PX: f32 = 4000.0;
const AXIS_LINE_THICKNESS_PX: f32 = 1.5;
const GRAB_Z_INDEX: i32 = 6_000;

const X_COLOR: Vec4 = Vec4::new(0.95, 0.30, 0.30, 1.0);
const Y_COLOR: Vec4 = Vec4::new(0.40, 0.95, 0.30, 1.0);
const Z_COLOR: Vec4 = Vec4::new(0.30, 0.55, 0.95, 1.0);
const FREE_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 0.45);

pub fn tick(editor_world: &mut EditorWorld, world: &mut World) {
    let keyboard = &world.resources.input.keyboard;
    let mouse = *nightshade::ecs::input::access::mouse_for_active(world);
    let g_pressed = keyboard.is_key_pressed(KeyCode::KeyG);
    let x_pressed = keyboard.is_key_pressed(KeyCode::KeyX);
    let y_pressed = keyboard.is_key_pressed(KeyCode::KeyY);
    let z_pressed = keyboard.is_key_pressed(KeyCode::KeyZ);
    let enter_pressed =
        keyboard.is_key_pressed(KeyCode::Enter) || keyboard.is_key_pressed(KeyCode::NumpadEnter);
    let escape_pressed = keyboard.is_key_pressed(KeyCode::Escape);
    let shift_held =
        keyboard.is_key_pressed(KeyCode::ShiftLeft) || keyboard.is_key_pressed(KeyCode::ShiftRight);
    let ctrl_held = keyboard.is_key_pressed(KeyCode::ControlLeft)
        || keyboard.is_key_pressed(KeyCode::ControlRight);
    let left_mouse_pressed = mouse.state.contains(MouseState::LEFT_JUST_PRESSED);
    let right_mouse_pressed = mouse.state.contains(MouseState::RIGHT_JUST_PRESSED);

    let g_edge =
        g_pressed && !editor_world.resources.grab.g_was_pressed && !ctrl_held && !shift_held;
    let x_edge = x_pressed && !editor_world.resources.grab.x_was_pressed;
    let y_edge = y_pressed && !editor_world.resources.grab.y_was_pressed;
    let z_edge = z_pressed && !editor_world.resources.grab.z_was_pressed;
    let enter_edge = enter_pressed && !editor_world.resources.grab.enter_was_pressed;
    let escape_edge = escape_pressed && !editor_world.resources.grab.escape_was_pressed;
    let left_click_edge = left_mouse_pressed;
    let right_click_edge = right_mouse_pressed;

    editor_world.resources.grab.g_was_pressed = g_pressed;
    editor_world.resources.grab.x_was_pressed = x_pressed;
    editor_world.resources.grab.y_was_pressed = y_pressed;
    editor_world.resources.grab.z_was_pressed = z_pressed;
    editor_world.resources.grab.enter_was_pressed = enter_pressed;
    editor_world.resources.grab.escape_was_pressed = escape_pressed;
    editor_world.resources.grab.left_mouse_was_pressed = left_mouse_pressed;
    editor_world.resources.grab.right_mouse_was_pressed = right_mouse_pressed;

    if editor_world.resources.grab.session.is_none() {
        if !g_edge {
            return;
        }
        if input::ui_capturing(world) {
            return;
        }
        if !input::mouse_in_active_viewport(world) {
            return;
        }
        try_start_session(editor_world, world, mouse.position);
        return;
    }

    if escape_edge || right_click_edge {
        cancel_session(editor_world, world);
        return;
    }

    if x_edge {
        toggle_constraint(editor_world, GrabAxis::X, shift_held);
        rebase_session_origin(editor_world, world, mouse.position);
    }
    if y_edge {
        toggle_constraint(editor_world, GrabAxis::Y, shift_held);
        rebase_session_origin(editor_world, world, mouse.position);
    }
    if z_edge {
        toggle_constraint(editor_world, GrabAxis::Z, shift_held);
        rebase_session_origin(editor_world, world, mouse.position);
    }

    apply_grab_movement(editor_world, world, mouse.position);

    if enter_edge || left_click_edge {
        commit_session(editor_world, world);
        return;
    }

    draw_constraint_overlay(editor_world, world);
}

pub fn is_active(editor_world: &EditorWorld) -> bool {
    editor_world.resources.grab.session.is_some()
}

fn try_start_session(editor_world: &mut EditorWorld, world: &World, mouse_position: Vec2) {
    if editor_world.resources.ui.selected_entities.is_empty() {
        return;
    }
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };
    let Some(primary) = editor_world.resources.ui.selected_entity else {
        return;
    };
    let Some(primary_transform) = world.core.get_global_transform(primary) else {
        return;
    };
    let primary_world_origin = primary_transform.translation();

    let Some((view_projection, viewport, camera_position)) = camera_view(world, camera_entity)
    else {
        return;
    };
    let inverse_view_projection = view_projection
        .try_inverse()
        .unwrap_or_else(nalgebra_glm::Mat4::identity);
    let camera_transform = match world.core.get_global_transform(camera_entity) {
        Some(transform) => transform,
        None => return,
    };
    let camera_forward = camera_transform.forward_vector();
    let plane_normal_world = camera_forward.normalize();

    let Some(initial_world_hit) = mouse_ray_plane_hit(
        mouse_position,
        &viewport,
        &inverse_view_projection,
        camera_position,
        primary_world_origin,
        plane_normal_world,
    ) else {
        return;
    };

    let mut entries: Vec<GrabEntry> = Vec::new();
    for entity in editor_world.resources.ui.selected_entities.clone() {
        let Some(local_transform) = world.core.get_local_transform(entity) else {
            continue;
        };
        let Some(global_transform) = world.core.get_global_transform(entity) else {
            continue;
        };
        entries.push(GrabEntry {
            entity,
            original_local_translation: local_transform.translation,
            reference_world_translation: global_transform.translation(),
        });
    }
    if entries.is_empty() {
        return;
    }

    editor_world.resources.grab.session = Some(GrabSession {
        entries,
        primary_world_origin,
        initial_world_hit,
        plane_normal_world,
        camera_entity,
        constraint: GrabConstraint::Free,
    });
}

fn toggle_constraint(editor_world: &mut EditorWorld, axis: GrabAxis, shift_held: bool) {
    let Some(session) = editor_world.resources.grab.session.as_mut() else {
        return;
    };
    let next = if shift_held {
        GrabConstraint::Plane(axis)
    } else {
        GrabConstraint::Axis(axis)
    };
    session.constraint = if session.constraint == next {
        GrabConstraint::Free
    } else {
        next
    };
}

fn apply_grab_movement(editor_world: &mut EditorWorld, world: &mut World, mouse_position: Vec2) {
    let session_clone = match editor_world.resources.grab.session.as_ref() {
        Some(session) => session.clone(),
        None => return,
    };

    let Some(current_hit) = current_mouse_hit(world, &session_clone, mouse_position) else {
        return;
    };

    let raw_delta = current_hit - session_clone.initial_world_hit;
    let mut constrained_delta = constrain_delta(raw_delta, session_clone.constraint);

    let snap = &editor_world.resources.snap;
    if snap.enabled && snap.translation_step > 0.0 {
        let step = snap.translation_step;
        let target = session_clone.primary_world_origin + constrained_delta;
        let snapped = Vec3::new(
            (target.x / step).round() * step,
            (target.y / step).round() * step,
            (target.z / step).round() * step,
        );
        constrained_delta = snapped - session_clone.primary_world_origin;
    }

    for entry in &session_clone.entries {
        let new_world_translation = entry.reference_world_translation + constrained_delta;
        let parent_inverse = world
            .core
            .get_parent(entry.entity)
            .and_then(|parent| parent.0)
            .and_then(|parent_entity| world.core.get_global_transform(parent_entity).copied())
            .map(|parent_global| {
                parent_global
                    .0
                    .try_inverse()
                    .unwrap_or_else(nalgebra_glm::Mat4::identity)
            });
        let new_local_translation = match parent_inverse {
            Some(inverse) => {
                let homogeneous = inverse
                    * nalgebra_glm::Vec4::new(
                        new_world_translation.x,
                        new_world_translation.y,
                        new_world_translation.z,
                        1.0,
                    );
                Vec3::new(homogeneous.x, homogeneous.y, homogeneous.z)
            }
            None => new_world_translation,
        };
        if let Some(transform) = world.core.get_local_transform_mut(entry.entity) {
            transform.translation = new_local_translation;
        }
        mark_local_transform_dirty(world, entry.entity);
    }
}

fn constrain_delta(delta: Vec3, constraint: GrabConstraint) -> Vec3 {
    match constraint {
        GrabConstraint::Free => delta,
        GrabConstraint::Axis(axis) => {
            let unit = axis.unit();
            unit * nalgebra_glm::dot(&delta, &unit)
        }
        GrabConstraint::Plane(axis) => {
            let unit = axis.unit();
            delta - unit * nalgebra_glm::dot(&delta, &unit)
        }
    }
}

fn current_mouse_hit(world: &World, session: &GrabSession, mouse_position: Vec2) -> Option<Vec3> {
    let (view_projection, viewport, camera_position) = camera_view(world, session.camera_entity)?;
    let inverse_view_projection = view_projection
        .try_inverse()
        .unwrap_or_else(nalgebra_glm::Mat4::identity);
    mouse_ray_plane_hit(
        mouse_position,
        &viewport,
        &inverse_view_projection,
        camera_position,
        session.primary_world_origin,
        session.plane_normal_world,
    )
}

fn rebase_session_origin(editor_world: &mut EditorWorld, world: &World, mouse_position: Vec2) {
    let session_snapshot = match editor_world.resources.grab.session.as_ref() {
        Some(session) => session.clone(),
        None => return,
    };
    let Some(new_hit) = current_mouse_hit(world, &session_snapshot, mouse_position) else {
        return;
    };
    let Some(active) = editor_world.resources.grab.session.as_mut() else {
        return;
    };
    for entry in active.entries.iter_mut() {
        if let Some(global) = world.core.get_global_transform(entry.entity) {
            entry.reference_world_translation = global.translation();
        }
    }
    if let Some(last_entry) = active.entries.last()
        && let Some(global) = world.core.get_global_transform(last_entry.entity)
    {
        active.primary_world_origin = global.translation();
    }
    active.initial_world_hit = new_hit;
}

fn camera_view(
    world: &World,
    camera_entity: Entity,
) -> Option<(nalgebra_glm::Mat4, ViewportRect, Vec3)> {
    let camera = world.core.get_camera(camera_entity).copied()?;
    let camera_transform = world.core.get_global_transform(camera_entity)?;
    let view_matrix = camera_transform
        .0
        .try_inverse()
        .unwrap_or_else(nalgebra_glm::Mat4::identity);
    let viewport = world
        .resources
        .window
        .camera_tile_rects
        .get(&camera_entity)
        .copied()
        .or(world.resources.window.active_viewport_rect)
        .filter(|rect| rect.width > 0.0 && rect.height > 0.0)?;
    let aspect = viewport.width / viewport.height;
    let projection_matrix = camera.projection.matrix_with_aspect(aspect);
    let view_projection = projection_matrix * view_matrix;
    Some((view_projection, viewport, camera_transform.translation()))
}

fn mouse_ray_plane_hit(
    mouse_position: Vec2,
    viewport: &ViewportRect,
    inverse_view_projection: &nalgebra_glm::Mat4,
    camera_position: Vec3,
    plane_origin: Vec3,
    plane_normal: Vec3,
) -> Option<Vec3> {
    let ndc_x = ((mouse_position.x - viewport.x) / viewport.width) * 2.0 - 1.0;
    let ndc_y = 1.0 - ((mouse_position.y - viewport.y) / viewport.height) * 2.0;
    let near_clip = inverse_view_projection * nalgebra_glm::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
    if near_clip.w.abs() < 1e-6 {
        return None;
    }
    let near_world = Vec3::new(near_clip.x, near_clip.y, near_clip.z) / near_clip.w;
    let direction = near_world - camera_position;
    let direction_magnitude = direction.magnitude();
    if direction_magnitude < 1e-6 {
        return None;
    }
    let direction_unit = direction / direction_magnitude;
    let denominator = nalgebra_glm::dot(&direction_unit, &plane_normal);
    if denominator.abs() < 1e-4 {
        return None;
    }
    let parameter =
        nalgebra_glm::dot(&(plane_origin - camera_position), &plane_normal) / denominator;
    if parameter < 0.0 {
        return None;
    }
    Some(camera_position + direction_unit * parameter)
}

fn cancel_session(editor_world: &mut EditorWorld, world: &mut World) {
    let Some(session) = editor_world.resources.grab.session.take() else {
        return;
    };
    for entry in &session.entries {
        if let Some(transform) = world.core.get_local_transform_mut(entry.entity) {
            transform.translation = entry.original_local_translation;
        }
        mark_local_transform_dirty(world, entry.entity);
    }
}

fn commit_session(editor_world: &mut EditorWorld, world: &mut World) {
    let Some(session) = editor_world.resources.grab.session.take() else {
        return;
    };
    let mut steps: Vec<crate::undo::UndoableOperation> = Vec::new();
    for entry in &session.entries {
        let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entry.entity) else {
            continue;
        };
        let Some(end) = world.core.get_local_transform(entry.entity).copied() else {
            continue;
        };
        let old = LocalTransform {
            translation: entry.original_local_translation,
            rotation: end.rotation,
            scale: end.scale,
        };
        if old.translation == end.translation {
            continue;
        }
        steps.push(crate::undo::UndoableOperation::TransformChanged {
            uuid,
            old,
            new: end,
        });
        crate::scene_writeback::sync_entity(
            &mut editor_world.resources.project,
            &editor_world.resources.editor_scene,
            world,
            entry.entity,
        );
    }
    if steps.is_empty() {
        return;
    }
    if steps.len() == 1 {
        let step = steps.into_iter().next().unwrap();
        editor_world.resources.undo.push(step, "Grab");
    } else {
        editor_world
            .resources
            .undo
            .push(crate::undo::UndoableOperation::Batch { steps }, "Grab");
    }
    editor_world.resources.ui_handles.inspector.dirty = true;
}

fn draw_constraint_overlay(editor_world: &EditorWorld, world: &mut World) {
    let Some(session) = editor_world.resources.grab.session.as_ref() else {
        return;
    };
    let Some((view_projection, viewport, _)) = camera_view(world, session.camera_entity) else {
        return;
    };
    let Some(origin_screen) = project(&view_projection, &viewport, session.primary_world_origin)
    else {
        return;
    };
    let clip = Rect::new(viewport.x, viewport.y, viewport.width, viewport.height);
    match session.constraint {
        GrabConstraint::Free => {
            push_marker(world, origin_screen, FREE_COLOR, Some(clip));
        }
        GrabConstraint::Axis(axis) => {
            let color = axis_color(axis);
            let direction = projected_axis_direction(
                &view_projection,
                &viewport,
                session.primary_world_origin,
                axis.unit(),
            );
            push_infinite_line(world, origin_screen, direction, color, Some(clip));
            push_marker(world, origin_screen, color, Some(clip));
        }
        GrabConstraint::Plane(axis) => {
            let other = perpendicular_axes(axis);
            for (other_axis, _) in &other {
                let color = axis_color(*other_axis);
                let direction = projected_axis_direction(
                    &view_projection,
                    &viewport,
                    session.primary_world_origin,
                    other_axis.unit(),
                );
                push_infinite_line(world, origin_screen, direction, color, Some(clip));
            }
            push_marker(world, origin_screen, axis_color(axis), Some(clip));
        }
    }
}

fn perpendicular_axes(locked: GrabAxis) -> [(GrabAxis, Vec3); 2] {
    match locked {
        GrabAxis::X => [
            (GrabAxis::Y, Vec3::new(0.0, 1.0, 0.0)),
            (GrabAxis::Z, Vec3::new(0.0, 0.0, 1.0)),
        ],
        GrabAxis::Y => [
            (GrabAxis::X, Vec3::new(1.0, 0.0, 0.0)),
            (GrabAxis::Z, Vec3::new(0.0, 0.0, 1.0)),
        ],
        GrabAxis::Z => [
            (GrabAxis::X, Vec3::new(1.0, 0.0, 0.0)),
            (GrabAxis::Y, Vec3::new(0.0, 1.0, 0.0)),
        ],
    }
}

fn axis_color(axis: GrabAxis) -> Vec4 {
    match axis {
        GrabAxis::X => X_COLOR,
        GrabAxis::Y => Y_COLOR,
        GrabAxis::Z => Z_COLOR,
    }
}

fn projected_axis_direction(
    view_projection: &nalgebra_glm::Mat4,
    viewport: &ViewportRect,
    origin_world: Vec3,
    axis_world: Vec3,
) -> Vec2 {
    let Some(origin_screen) = project(view_projection, viewport, origin_world) else {
        return Vec2::new(1.0, 0.0);
    };
    let Some(tip_screen) = project(view_projection, viewport, origin_world + axis_world) else {
        return Vec2::new(1.0, 0.0);
    };
    let delta = tip_screen - origin_screen;
    let magnitude = delta.magnitude();
    if magnitude < 1e-4 {
        Vec2::new(1.0, 0.0)
    } else {
        delta / magnitude
    }
}

fn project(
    view_projection: &nalgebra_glm::Mat4,
    viewport: &ViewportRect,
    world_position: Vec3,
) -> Option<Vec2> {
    let clip = view_projection
        * nalgebra_glm::Vec4::new(world_position.x, world_position.y, world_position.z, 1.0);
    if clip.w <= 0.0 {
        return None;
    }
    let ndc_x = clip.x / clip.w;
    let ndc_y = clip.y / clip.w;
    let screen_x = viewport.x + (ndc_x + 1.0) * 0.5 * viewport.width;
    let screen_y = viewport.y + (1.0 - ndc_y) * 0.5 * viewport.height;
    Some(Vec2::new(screen_x, screen_y))
}

fn push_infinite_line(
    world: &mut World,
    origin: Vec2,
    direction: Vec2,
    color: Vec4,
    clip: Option<Rect>,
) {
    let from = origin - direction * AXIS_LINE_LENGTH_PX;
    let to = origin + direction * AXIS_LINE_LENGTH_PX;
    push_segment(world, from, to, color, AXIS_LINE_THICKNESS_PX, clip);
}

fn push_segment(
    world: &mut World,
    from: Vec2,
    to: Vec2,
    color: Vec4,
    thickness: f32,
    clip: Option<Rect>,
) {
    let delta = to - from;
    let length = delta.magnitude();
    if length < 0.001 {
        return;
    }
    let midpoint = (from + to) * 0.5;
    let angle = delta.y.atan2(delta.x);
    let position = midpoint - Vec2::new(length * 0.5, thickness * 0.5);
    world.resources.retained_ui.frame.rects.push(UiRect {
        position,
        size: Vec2::new(length, thickness),
        color,
        corner_radius: 0.0,
        border_width: 0.0,
        border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
        rotation: angle,
        clip_rect: clip,
        layer: UiLayer::Background,
        z_index: GRAB_Z_INDEX,
        shadow: None,
        effect_kind: 0,
        effect_params: [0.0; 4],
        quad_corners: None,
    });
}

fn push_marker(world: &mut World, origin: Vec2, color: Vec4, clip: Option<Rect>) {
    let size = 8.0;
    let half = size * 0.5;
    world.resources.retained_ui.frame.rects.push(UiRect {
        position: origin - Vec2::new(half, half),
        size: Vec2::new(size, size),
        color,
        corner_radius: half,
        border_width: 0.0,
        border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
        rotation: 0.0,
        clip_rect: clip,
        layer: UiLayer::Background,
        z_index: GRAB_Z_INDEX + 1,
        shadow: None,
        effect_kind: 0,
        effect_params: [0.0; 4],
        quad_corners: None,
    });
}
