mod cluster;
mod monitor;
mod node;
mod trail;
mod view;

use std::time::Instant;

use halley_ipc::{
    BearingsRequest, BearingsStatusResponse, CaptureRequest, CaptureStatusResponse,
    CompositorRequest, IpcError, NodeMoveDirection, Request, Response, StackRequest, TileRequest,
};

use crate::compositor::root::Halley;
use crate::compositor::screenshot::screenshot_controller;

use self::cluster::handle_cluster_request;
use self::monitor::handle_monitor_request;
use self::node::handle_node_request;
use self::trail::handle_trail_request;
use self::view::IpcView;

#[cfg(test)]
use self::cluster::{inspect_cluster, list_clusters};
#[cfg(test)]
use self::monitor::adjacent_monitor;
#[cfg(test)]
use self::node::resolve_node_selector;

pub(crate) fn handle_request(st: &mut Halley, request: Request) -> Response {
    match request {
        Request::Capture(request) => handle_capture_request(st, request),
        Request::Node(request) => handle_node_request(st, request),
        Request::Trail(request) => handle_trail_request(st, request),
        Request::Monitor(request) => handle_monitor_request(st, request),
        Request::Bearings(request) => handle_bearings_request(st, request),
        Request::Stack(request) => handle_stack_request(st, request),
        Request::Tile(request) => handle_tile_request(st, request),
        Request::Cluster(request) => handle_cluster_request(st, request),
        Request::Compositor(CompositorRequest::Outputs) => Response::Error(IpcError::Unsupported(
            "outputs are handled by the ipc listener".into(),
        )),
        Request::Compositor(CompositorRequest::ApertureStatus) => {
            Response::ApertureStatus(crate::aperture::aperture_status(st))
        }
        Request::Compositor(CompositorRequest::Quit)
        | Request::Compositor(CompositorRequest::Reload)
        | Request::Compositor(CompositorRequest::Dpms { .. }) => Response::Error(
            IpcError::Unsupported("backend request not handled here".into()),
        ),
    }
}

fn handle_capture_request(st: &mut Halley, request: CaptureRequest) -> Response {
    match request {
        CaptureRequest::Start { mode, output } => {
            if screenshot_controller(&mut *st).start_screenshot_session(
                mode,
                output.as_deref(),
                Instant::now(),
            ) {
                Response::CaptureStatus(capture_status_response(st))
            } else {
                Response::Error(IpcError::Unsupported(
                    "screenshot session is already active".into(),
                ))
            }
        }
        CaptureRequest::Status => Response::CaptureStatus(capture_status_response(st)),
    }
}

fn capture_status_response(st: &Halley) -> CaptureStatusResponse {
    let last = st.input.interaction_state.last_screenshot_result.as_ref();
    CaptureStatusResponse {
        active: screenshot_controller(st).screenshot_session_active()
            || st
                .input
                .interaction_state
                .pending_screenshot_capture
                .is_some()
            || st
                .input
                .interaction_state
                .inflight_screenshot_capture
                .is_some(),
        session_serial: st
            .input
            .interaction_state
            .screenshot_session
            .as_ref()
            .map(|_| {
                st.input
                    .interaction_state
                    .screenshot_next_serial
                    .saturating_sub(1)
            }),
        last_finished_serial: last.map(|result| result.serial),
        saved_path: last
            .and_then(|result| result.saved_path.as_ref())
            .map(|path| path.display().to_string()),
        error: last.and_then(|result| result.error.clone()),
    }
}

fn handle_bearings_request(st: &mut Halley, request: BearingsRequest) -> Response {
    match request {
        BearingsRequest::Show => {
            st.ui.render_state.set_bearings_visible(true);
            Response::Ok
        }
        BearingsRequest::Hide => {
            st.ui.render_state.set_bearings_visible(false);
            Response::Ok
        }
        BearingsRequest::Toggle => {
            st.ui.render_state.toggle_bearings_visible();
            Response::Ok
        }
        BearingsRequest::Status => Response::BearingsStatus(BearingsStatusResponse {
            visible: st.ui.render_state.bearings_visible(),
        }),
    }
}

fn handle_stack_request(st: &mut Halley, request: StackRequest) -> Response {
    match request {
        StackRequest::Cycle { direction, output } => {
            match resolve_output_context(st, output.as_deref()).and_then(|monitor| {
                let now = Instant::now();
                focus_output_if_needed(st, monitor.as_str(), now);
                let direction = match direction {
                    halley_ipc::StackCycleDirection::Forward => {
                        halley_core::cluster_layout::ClusterCycleDirection::Next
                    }
                    halley_ipc::StackCycleDirection::Backward => {
                        halley_core::cluster_layout::ClusterCycleDirection::Prev
                    }
                };
                if st.cycle_active_stack_for_monitor(monitor.as_str(), direction, now) {
                    Ok(())
                } else {
                    Err(IpcError::Unsupported(format!(
                        "stack layout is not active on output {}",
                        monitor
                    )))
                }
            }) {
                Ok(()) => Response::Ok,
                Err(err) => Response::Error(err),
            }
        }
    }
}

fn handle_tile_request(st: &mut Halley, request: TileRequest) -> Response {
    let (direction, output, swap) = match request {
        TileRequest::Focus { direction, output } => (direction, output, false),
        TileRequest::Swap { direction, output } => (direction, output, true),
    };
    match resolve_output_context(st, output.as_deref()).and_then(|monitor| {
        let now = Instant::now();
        focus_output_if_needed(st, monitor.as_str(), now);
        let direction = match direction {
            NodeMoveDirection::Left => halley_config::DirectionalAction::Left,
            NodeMoveDirection::Right => halley_config::DirectionalAction::Right,
            NodeMoveDirection::Up => halley_config::DirectionalAction::Up,
            NodeMoveDirection::Down => halley_config::DirectionalAction::Down,
        };
        let ok = if swap {
            st.tile_swap_active_cluster_member_for_monitor(monitor.as_str(), direction, now)
        } else {
            st.tile_focus_active_cluster_member_for_monitor(monitor.as_str(), direction, now)
        };
        if ok {
            Ok(())
        } else {
            Err(IpcError::Unsupported(format!(
                "tiled layout is not active or no tile exists {} on output {}",
                if swap { "to swap" } else { "to focus" },
                monitor
            )))
        }
    }) {
        Ok(()) => Response::Ok,
        Err(err) => Response::Error(err),
    }
}

fn resolve_output_context(st: &Halley, output: Option<&str>) -> Result<String, IpcError> {
    IpcView::from_halley(st).resolve_output_context(output)
}

fn validate_output<'a>(st: &Halley, output: &'a str) -> Result<&'a str, IpcError> {
    IpcView::from_halley(st).validate_output(output)?;
    Ok(output)
}

fn focus_output_if_needed(st: &mut Halley, output: &str, now: Instant) {
    if st.focused_monitor() != output {
        st.focus_monitor_view(output, now);
    }
}

fn sorted_outputs(st: &Halley) -> Vec<String> {
    IpcView::from_halley(st).sorted_outputs()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::compositor::clusters::state::ClusterNameRecord;
    use halley_core::cluster::ClusterId;
    use halley_core::field::{NodeId, Vec2};

    fn cluster_test_state() -> (Halley, ClusterId, NodeId, NodeId) {
        let tuning = halley_config::RuntimeTuning::default();
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut state = Halley::new_for_test(&dh, tuning);
        let first = state.model.field.spawn_surface(
            "firefox",
            Vec2 { x: 0.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        let second = state.model.field.spawn_surface(
            "foot",
            Vec2 { x: 200.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        state.assign_node_to_current_monitor(first);
        state.assign_node_to_current_monitor(second);
        let cid = state
            .model
            .field
            .create_cluster(vec![first, second])
            .expect("cluster");
        state.model.cluster_state.cluster_names.insert(
            cid,
            ClusterNameRecord::Custom {
                name: "web".to_string(),
            },
        );
        let monitor = state.model.monitor_state.current_monitor.clone();
        state
            .model
            .cluster_state
            .active_cluster_workspaces
            .insert(monitor.clone(), cid);
        state
            .model
            .cluster_state
            .cluster_slot_order
            .insert(monitor, vec![cid]);
        state
            .model
            .field
            .cluster_mut(cid)
            .expect("cluster mut")
            .enter_active();
        state.model.focus_state.primary_interaction_focus = Some(second);
        (state, cid, first, second)
    }

    fn cluster_slot_test_state() -> (Halley, ClusterId, NodeId, String) {
        let tuning = halley_config::RuntimeTuning::default();
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut state = Halley::new_for_test(&dh, tuning);
        let first = state.model.field.spawn_surface(
            "first",
            Vec2 { x: 0.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        let second = state.model.field.spawn_surface(
            "second",
            Vec2 { x: 200.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        state.assign_node_to_current_monitor(first);
        state.assign_node_to_current_monitor(second);
        let cid = state
            .model
            .field
            .create_cluster(vec![first, second])
            .expect("cluster");
        let core = state.model.field.collapse_cluster(cid).expect("core");
        state.assign_node_to_current_monitor(core);
        let monitor = state.model.monitor_state.current_monitor.clone();
        state
            .model
            .cluster_state
            .cluster_names
            .insert(cid, ClusterNameRecord::Generic { slot: 1 });
        state
            .model
            .cluster_state
            .cluster_slot_order
            .insert(monitor, vec![cid]);
        let core_pos = state.model.field.node(core).expect("core node").pos;
        state.model.viewport.center = core_pos;
        state.model.camera_target_center = core_pos;
        let monitor = state.model.monitor_state.current_monitor.clone();
        (state, cid, core, monitor)
    }

    #[test]
    fn default_selector_falls_back_to_latest_on_output() {
        let tuning = halley_config::RuntimeTuning::default();
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut state = Halley::new_for_test(&dh, tuning);
        let first = state.model.field.spawn_surface(
            "first",
            Vec2 { x: 0.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        let second = state.model.field.spawn_surface(
            "second",
            Vec2 { x: 200.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        state.assign_node_to_current_monitor(first);
        state.assign_node_to_current_monitor(second);
        state.model.focus_state.primary_interaction_focus = None;

        assert_eq!(
            resolve_node_selector(&state, None, None).unwrap().as_u64(),
            2
        );
    }

    #[test]
    fn title_selector_reports_ambiguity() {
        let tuning = halley_config::RuntimeTuning::default();
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut state = Halley::new_for_test(&dh, tuning);
        let first = state.model.field.spawn_surface(
            "Kitty",
            Vec2 { x: 0.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        let second = state.model.field.spawn_surface(
            "Kitty scratch",
            Vec2 { x: 200.0, y: 0.0 },
            Vec2 { x: 100.0, y: 80.0 },
        );
        state.assign_node_to_current_monitor(first);
        state.assign_node_to_current_monitor(second);

        let result = resolve_node_selector(
            &state,
            Some(&halley_ipc::NodeSelector::Title("kitty".into())),
            None,
        );
        assert!(matches!(result, Err(IpcError::Ambiguous(_))));
    }

    #[test]
    fn monitor_focus_direction_picks_adjacent_output() {
        let mut tuning = halley_config::RuntimeTuning::default();
        tuning.tty_viewports = vec![
            halley_config::ViewportOutputConfig {
                connector: "left".to_string(),
                enabled: true,
                offset_x: 0,
                offset_y: 0,
                width: 1920,
                height: 1080,
                refresh_rate: None,
                transform_degrees: 0,
                vrr: halley_config::ViewportVrrMode::Off,
                focus_ring: None,
            },
            halley_config::ViewportOutputConfig {
                connector: "right".to_string(),
                enabled: true,
                offset_x: 1920,
                offset_y: 0,
                width: 1920,
                height: 1080,
                refresh_rate: None,
                transform_degrees: 0,
                vrr: halley_config::ViewportVrrMode::Off,
                focus_ring: None,
            },
        ];
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut state = Halley::new_for_test(&dh, tuning);
        state.focus_monitor_view("left", Instant::now());

        assert_eq!(
            adjacent_monitor(&state, halley_ipc::MonitorFocusDirection::Right).as_deref(),
            Some("right")
        );
    }

    #[test]
    fn cluster_list_reports_named_active_cluster() {
        let (state, cid, _, _) = cluster_test_state();

        let list = list_clusters(&state, None).expect("cluster list");
        let cluster = list
            .outputs
            .iter()
            .flat_map(|group| group.clusters.iter())
            .find(|cluster| cluster.id == cid.as_u64())
            .expect("cluster summary");

        assert_eq!(cluster.name.as_deref(), Some("web"));
        assert_eq!(cluster.slot, Some(1));
        assert!(cluster.active);
        assert!(cluster.focused);
        assert_eq!(cluster.member_count, 2);
    }

    #[test]
    fn cluster_inspect_defaults_to_current_output_active_cluster() {
        let (state, cid, first, second) = cluster_test_state();

        let info = inspect_cluster(&state, None, None).expect("cluster inspect");

        assert_eq!(info.id, cid.as_u64());
        assert_eq!(info.slot, Some(1));
        assert_eq!(info.name.as_deref(), Some("web"));
        assert!(info.active);
        assert!(info.focused);
        assert_eq!(info.focused_member_index, Some(1));
        assert_eq!(info.focused_member_id, Some(second.as_u64()));
        assert_eq!(info.members.len(), 2);
        assert_eq!(info.members[0].id, first.as_u64());
        assert_eq!(info.members[1].id, second.as_u64());
    }

    #[test]
    fn cluster_slot_request_activates_slot() {
        let (mut state, cid, _, monitor) = cluster_slot_test_state();

        let response = handle_request(
            &mut state,
            halley_ipc::Request::Cluster(halley_ipc::ClusterRequest::Slot {
                slot: 1,
                output: None,
            }),
        );

        assert!(matches!(response, Response::Ok));
        assert_eq!(
            state.active_cluster_workspace_for_monitor(monitor.as_str()),
            Some(cid)
        );
    }

    #[test]
    fn cluster_slot_request_toggles_active_slot_closed() {
        let (mut state, _, core, monitor) = cluster_slot_test_state();

        let first = handle_request(
            &mut state,
            halley_ipc::Request::Cluster(halley_ipc::ClusterRequest::Slot {
                slot: 1,
                output: None,
            }),
        );
        let second = handle_request(
            &mut state,
            halley_ipc::Request::Cluster(halley_ipc::ClusterRequest::Slot {
                slot: 1,
                output: None,
            }),
        );

        assert!(matches!(first, Response::Ok));
        assert!(matches!(second, Response::Ok));
        assert_eq!(
            state.active_cluster_workspace_for_monitor(monitor.as_str()),
            None
        );
        assert_eq!(
            state.model.focus_state.primary_interaction_focus,
            Some(core)
        );
    }

    #[test]
    fn cluster_slot_request_rejects_invalid_slot() {
        let (mut state, _, _, _) = cluster_slot_test_state();

        let response = handle_request(
            &mut state,
            halley_ipc::Request::Cluster(halley_ipc::ClusterRequest::Slot {
                slot: 0,
                output: None,
            }),
        );

        assert!(matches!(
            response,
            Response::Error(IpcError::InvalidRequest(_))
        ));
    }

    #[test]
    fn cluster_slot_request_reports_empty_slot() {
        let tuning = halley_config::RuntimeTuning::default();
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut state = Halley::new_for_test(&dh, tuning);

        let response = handle_request(
            &mut state,
            halley_ipc::Request::Cluster(halley_ipc::ClusterRequest::Slot {
                slot: 1,
                output: None,
            }),
        );

        assert!(matches!(response, Response::Error(IpcError::NotFound(_))));
    }
}
