use std::cell::RefCell;
use std::cmp;
use std::rc::{Rc, Weak};

use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{LoopHandle, RegistrationToken};
use smithay::utils::{Logical, Point, Rectangle, Size};

use crate::drawing::{ForgeElement, Graphics};
use crate::forge::Forge;
use crate::geometry::Vector;
use crate::input::HOLD_DURATION;
use crate::output::{Canvas, Output};
use crate::windows::layout::{LayoutPosition, Layouts};
use crate::windows::window::Window;

const PRIMARY_PERCENTAGE: f64 = 0.75;
const SECONDARY_PERCENTAGE: f64 = 0.95;
const DRAG_AND_DROP_PERCENTAGE: f64 = 0.3;
const OVERVIEW_CLOSE_DISTANCE: f64 = 0.25;
const OVERDRAG_LIMIT: f64 = 0.25;
const MINIMIZE_PERCENTAGE: f64 = 0.1;
const URGENCY_BAR_HEIGHT: i32 = 16;

#[derive(Debug)]
pub struct Overview {
    pub hold_timer: Option<RegistrationToken>,
    pub last_drag_point: Point<f64, Logical>,
    pub drag_action: DragAction,
    pub primary_percentage: f64,
    pub x_offset: f64,
    pub y_offset: f64,
    pub opened: bool,
    pub dirty: bool,
}

impl Overview {
    pub fn new(active_offset: f64, primary_percentage: Option<f64>) -> Self {
        let (primary_percentage, opened) = primary_percentage
            .map(|p| (p, false))
            .unwrap_or((PRIMARY_PERCENTAGE, true));

        Self {
            primary_percentage,
            opened,
            x_offset: active_offset,
            last_drag_point: Point::default(),
            drag_action: DragAction::default(),
            hold_timer: None,
            y_offset: 0.0,
            dirty: false,
        }
    }

    pub fn set_open_percentage(&mut self, output: &Output, mut position: f64) {
        let available = output.available_overview().to_f64();
        position = position.clamp(available.loc.y, available.loc.y + available.size.h);

        let mut delta = 1.0 - (position - available.loc.y) / available.size.h;
        delta *= 2.0;

        if self.opened {
            self.primary_percentage = PRIMARY_PERCENTAGE - delta;
        } else if self.primary_percentage < PRIMARY_PERCENTAGE {
            let pct = delta * PRIMARY_PERCENTAGE / (1.0 - PRIMARY_PERCENTAGE);
            self.primary_percentage = pct.min(PRIMARY_PERCENTAGE);
        } else if self.primary_percentage > PRIMARY_PERCENTAGE {
            self.primary_percentage = (1.0 - delta).max(PRIMARY_PERCENTAGE);
        }

        self.dirty = true;
    }

    /// Drive the foreground window's shrink directly from a bottom
    /// swipe-up progress `[0, 1]` — Android-style follow-the-finger from
    /// full size toward minimised, rather than the two-phase
    /// `set_open_percentage` mapping tuned for the overview grid.
    pub fn set_swipe_progress(&mut self, progress: f64) {
        let progress = progress.clamp(0.0, 1.0);
        self.primary_percentage =
            (1.0 - progress * (1.0 - MINIMIZE_PERCENTAGE)).clamp(MINIMIZE_PERCENTAGE, 1.0);
        self.dirty = true;
    }

    pub fn gesture_threshold_passed(&mut self) -> bool {
        let open_done = !self.opened && self.primary_percentage == PRIMARY_PERCENTAGE;
        let minimize_done = self.opened && self.primary_percentage <= MINIMIZE_PERCENTAGE;
        open_done || minimize_done
    }

    pub fn start_hold(
        &mut self,
        event_loop: &LoopHandle<'static, Forge>,
        layout_position: LayoutPosition,
    ) {
        self.cancel_hold(event_loop);

        let timer = Timer::from_duration(HOLD_DURATION);
        let token = event_loop
            .insert_source(timer, move |_, _, forge| {
                forge.windows.start_dnd(layout_position);
                forge.unstall();
                TimeoutAction::Drop
            })
            .expect("D&D hold timer");
        self.hold_timer = Some(token);
    }

    pub fn cancel_hold(&mut self, event_loop: &LoopHandle<'static, Forge>) {
        if let Some(token) = self.hold_timer.take() {
            event_loop.remove(token);
        }
    }

    pub fn layout_position(
        &self,
        canvas: &Canvas,
        layouts: &Layouts,
        point: Point<f64, Logical>,
    ) -> Option<LayoutPosition> {
        let available = canvas.available_overview();
        let scale = canvas.scale();

        let mut offset = self.x_offset - 1.0;
        while offset < self.x_offset + 2.0 {
            let position =
                OverviewPosition::new(available, self.primary_percentage, self.x_offset, offset);

            if position.layout_bounds().to_f64().contains(point) {
                let layout_index = usize::try_from(-offset.round() as isize).ok()?;
                let layout = layouts.get(layout_index)?;

                let contains = |w: &Rc<RefCell<Window>>| {
                    let bounds = w.borrow().bounds(scale);
                    position.window_bounds(bounds).to_f64().contains(point)
                };

                if layout.secondary().is_some_and(contains) {
                    return Some(LayoutPosition::new(layout_index, true));
                }

                if layout.primary().is_some_and(contains) {
                    return Some(LayoutPosition::new(layout_index, false));
                }
            }

            offset += 1.0;
        }

        None
    }

    fn clamp_offset(&mut self, window_count: i32) {
        let min_offset = -window_count as f64 + 1.0;
        self.x_offset = self
            .x_offset
            .clamp(min_offset - OVERDRAG_LIMIT, OVERDRAG_LIMIT);

        if self.drag_action.done {
            self.x_offset = self.x_offset.round().clamp(min_offset, 0.0);
        }
    }

    pub fn textures(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        output: &Output,
        canvas: &Canvas,
        layouts: &Layouts,
        graphics: &Graphics,
    ) {
        self.dirty = false;
        self.clamp_offset(layouts.len() as i32);

        let available = canvas.available_overview();

        let mut offset = self.x_offset - 1.0;
        while offset < self.x_offset + 2.0 {
            let layout_index = usize::try_from(-offset.round() as isize).ok();

            let Some(layout) = layout_index.and_then(|i| layouts.get(i)) else {
                offset += 1.0;
                continue;
            };

            let position =
                OverviewPosition::new(available, self.primary_percentage, self.x_offset, offset);

            if let Some(secondary) = layout.secondary() {
                self.window_textures(
                    textures, output, canvas, layouts, graphics, secondary, &position, false,
                );
            }

            if let Some(primary) = layout.primary() {
                self.window_textures(
                    textures, output, canvas, layouts, graphics, primary, &position, true,
                );
            }

            offset += 1.0;
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn window_textures(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        output: &Output,
        canvas: &Canvas,
        layouts: &Layouts,
        graphics: &Graphics,
        window: &Rc<RefCell<Window>>,
        position: &OverviewPosition,
        is_primary: bool,
    ) {
        let scale = canvas.scale();

        let mut bounds = position.window_bounds(window.borrow().bounds(scale));
        let closing_offset = self.handle_closing(output, canvas, layouts, window, is_primary);
        bounds.loc.y += closing_offset;

        let window = window.borrow();
        if !window.alive() {
            return;
        }

        let mut layout_bounds = position.layout_bounds();
        layout_bounds.loc.y += closing_offset;
        let physical_bounds = layout_bounds.to_physical_precise_round(scale);

        if window.urgent {
            let texture = graphics.urgency_icon.clone();
            let mut bounds = physical_bounds;
            bounds.size.h = (URGENCY_BAR_HEIGHT as f64 * position.scale).round() as i32;
            bounds.loc.y -= bounds.size.h;
            let window_scale = Some(cmp::max(bounds.size.w, bounds.size.h) as f64);

            ForgeElement::add_element(textures, texture, bounds.loc, bounds, window_scale, scale);
        }

        window.textures_with_bounds(
            textures,
            scale,
            position.scale,
            bounds.loc,
            physical_bounds,
            None,
        );
    }

    fn handle_closing(
        &mut self,
        output: &Output,
        canvas: &Canvas,
        layouts: &Layouts,
        window: &Rc<RefCell<Window>>,
        is_primary: bool,
    ) -> i32 {
        let closing = self.drag_action.closing_window();
        if !Weak::ptr_eq(&closing, &Rc::downgrade(window)) {
            return 0;
        }

        if is_primary {
            self.y_offset = self.y_offset.min(0.0);
        } else {
            self.y_offset = self.y_offset.max(0.0);
        }

        let delta = self.y_offset.round() as i32;

        if self.drag_action.done {
            let close_distance =
                canvas.available_overview().size.h as f64 * OVERVIEW_CLOSE_DISTANCE;

            if self.y_offset.abs() >= close_distance {
                let surface = {
                    let mut w = window.borrow_mut();
                    w.kill();
                    w.surface.clone()
                };
                layouts.reap(output, &surface);
            } else {
                self.y_offset = 0.0;
            }

            self.dirty = true;
        }

        delta
    }

    pub fn cycle_edge_reached(&self, window_count: usize) -> bool {
        let min_offset = -(window_count as f64) + 1.0;
        self.x_offset <= min_offset || self.x_offset >= 0.0
    }

    pub fn dirty(&self) -> bool {
        self.dirty
    }
}

#[derive(Clone, Debug)]
pub struct DragAndDrop {
    pub window_position: Point<f64, Logical>,
    pub touch_position: Point<f64, Logical>,
    pub window: Rc<RefCell<Window>>,
    pub overview_x_offset: f64,
    start_location: Point<i32, Logical>,
    scale: f64,
}

impl DragAndDrop {
    pub fn new(
        canvas: &Canvas,
        overview: &Overview,
        layout_position: LayoutPosition,
        window: Rc<RefCell<Window>>,
    ) -> Self {
        let window_x_offset =
            -(layout_position.index as f64) + (overview.x_offset - overview.x_offset.round());

        let available = canvas.available_overview();
        let position = OverviewPosition::new(
            available,
            PRIMARY_PERCENTAGE,
            overview.x_offset,
            window_x_offset,
        );

        let loc = window.borrow().bounds(canvas.scale()).loc;
        let start_location = position.window_loc(loc);

        Self {
            start_location,
            window,
            touch_position: overview.last_drag_point,
            overview_x_offset: overview.x_offset,
            scale: position.scale,
            window_position: Point::default(),
        }
    }

    pub fn textures(&self, textures: &mut Vec<ForgeElement>, canvas: &Canvas, graphics: &Graphics) {
        let scale = canvas.scale();
        let available = canvas.available_overview().to_f64().to_physical(scale);

        let (primary_bounds, secondary_bounds) = self.drop_bounds(canvas);

        let area_scale = available.size.w.max(available.size.h);
        for bounds in [primary_bounds, secondary_bounds] {
            let texture = if bounds.to_f64().contains(self.touch_position) {
                graphics.active_drop_target.clone()
            } else {
                graphics.drop_target.clone()
            };

            let bounds = bounds.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, texture, bounds.loc, bounds, area_scale, scale);
        }

        let location = self.start_location + self.window_position.to_i32_round();
        self.window
            .borrow()
            .textures(textures, scale, self.scale, location);
    }

    pub fn drop_bounds(
        &self,
        canvas: &Canvas,
    ) -> (Rectangle<i32, Logical>, Rectangle<i32, Logical>) {
        let available = canvas.available_overview();

        if available.size.h > available.size.w {
            let dnd_height = (available.size.h as f64 * DRAG_AND_DROP_PERCENTAGE).round() as i32;
            let size = Size::from((available.size.w, dnd_height));
            let primary = Rectangle::new(available.loc, size);
            let mut secondary = primary;
            secondary.loc.y += available.size.h - dnd_height;
            (primary, secondary)
        } else {
            let dnd_width = (available.size.w as f64 * DRAG_AND_DROP_PERCENTAGE).round() as i32;
            let size = Size::from((dnd_width, available.size.h));
            let primary = Rectangle::new(available.loc, size);
            let mut secondary = primary;
            secondary.loc.x += available.size.w - dnd_width;
            (primary, secondary)
        }
    }
}

#[derive(Default, Debug)]
pub struct DragAction {
    pub action_type: DragActionType,
    pub done: bool,
}

impl DragAction {
    pub fn closing_window(&self) -> Weak<RefCell<Window>> {
        match &self.action_type {
            DragActionType::Close(w) => w.clone(),
            _ => Weak::new(),
        }
    }
}

impl From<DragActionType> for DragAction {
    fn from(action_type: DragActionType) -> Self {
        Self {
            action_type,
            done: false,
        }
    }
}

#[derive(Default, Debug)]
pub enum DragActionType {
    Close(Weak<RefCell<Window>>),
    Cycle,
    #[default]
    None,
}

#[derive(Debug)]
struct OverviewPosition {
    layout_bounds: Rectangle<i32, Logical>,
    available_start: Point<i32, Logical>,
    offset: Point<i32, Logical>,
    scale: f64,
}

impl OverviewPosition {
    fn new(
        available: Rectangle<i32, Logical>,
        primary_pct: f64,
        center_offset_x: f64,
        window_offset: f64,
    ) -> Self {
        let delta = center_offset_x - window_offset.round();

        let secondary_pct = primary_pct * SECONDARY_PERCENTAGE;
        let scale = secondary_pct + (primary_pct - secondary_pct) * (1.0 - delta.abs());

        let mut offset = available.size.scale((1.0 - scale) / 2.0).to_point();
        offset.x += (delta * available.size.w as f64 * primary_pct) as i32;

        let mut layout_bounds = available;
        layout_bounds.size = layout_bounds.size.scale(scale);
        layout_bounds.loc += offset;

        Self {
            layout_bounds,
            available_start: available.loc,
            offset,
            scale,
        }
    }

    #[must_use]
    fn window_loc(&self, mut loc: Point<i32, Logical>) -> Point<i32, Logical> {
        loc -= self.available_start;
        loc.x = (loc.x as f64 * self.scale).trunc() as i32;
        loc.y = (loc.y as f64 * self.scale).trunc() as i32;
        loc += self.available_start;
        loc += self.offset;
        loc
    }

    #[must_use]
    fn window_bounds(&self, mut bounds: Rectangle<i32, Logical>) -> Rectangle<i32, Logical> {
        bounds.loc = self.window_loc(bounds.loc);
        bounds.size = bounds.size.scale(self.scale);
        bounds
    }

    fn layout_bounds(&self) -> Rectangle<i32, Logical> {
        self.layout_bounds
    }
}
