//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale.

use crate::consts::PIVOT_DIAMETER;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::path_tool::PathOptionsUpdate;
use crate::messages::tool::tool_messages::select_tool::SelectOptionsUpdate;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::{DAffine2, DVec2};
use graphene_std::transform::ReferencePoint;
use graphene_std::vector::misc::ManipulatorPointId;
use std::fmt;

pub fn pin_pivot_widget(active: bool, enabled: bool, source: PivotToolSource) -> WidgetInstance {
	IconButton::new(if active { "PinActive" } else { "PinInactive" }, 24)
		.tooltip_label(if active { "Unpin Custom Pivot" } else { "Pin Custom Pivot" })
		.tooltip_description("Unless pinned, the pivot will return to its prior reference point when a new selection is made.")
		.disabled(!enabled)
		.on_update(move |_| match source {
			PivotToolSource::Select => SelectToolMessage::SelectOptions {
				options: SelectOptionsUpdate::TogglePivotPinned,
			}
			.into(),
			PivotToolSource::Path => PathToolMessage::UpdateOptions {
				options: PathOptionsUpdate::TogglePivotPinned,
			}
			.into(),
		})
		.widget_instance()
}

pub fn pivot_reference_point_widget(disabled: bool, reference_point: ReferencePoint, source: PivotToolSource) -> WidgetInstance {
	ReferencePointInput::new(reference_point)
		.tooltip_label("Custom Pivot Reference Point")
		.tooltip_description("Places the pivot at a corner, edge, or center of the selection bounds, unless it is dragged elsewhere.")
		.disabled(disabled)
		.on_update(move |pivot_input: &ReferencePointInput| match source {
			PivotToolSource::Select => SelectToolMessage::SetPivot { position: pivot_input.value }.into(),
			PivotToolSource::Path => PathToolMessage::SetPivot { position: pivot_input.value }.into(),
		})
		.widget_instance()
}

pub fn pivot_gizmo_type_widget(state: PivotGizmoState, source: PivotToolSource) -> Vec<WidgetInstance> {
	let gizmo_type_entries = [PivotGizmoType::Pivot, PivotGizmoType::Average, PivotGizmoType::Active]
		.iter()
		.map(|gizmo_type| {
			MenuListEntry::new(format!("{gizmo_type:?}")).label(gizmo_type.to_string()).on_commit({
				let value = source.clone();
				move |_| match value {
					PivotToolSource::Select => SelectToolMessage::SelectOptions {
						options: SelectOptionsUpdate::PivotGizmoType(*gizmo_type),
					}
					.into(),
					PivotToolSource::Path => PathToolMessage::UpdateOptions {
						options: PathOptionsUpdate::PivotGizmoType(*gizmo_type),
					}
					.into(),
				}
			})
		})
		.collect();

	vec![
		CheckboxInput::new(state.enabled)
			.tooltip_label("Pivot Gizmo")
			.tooltip_description(
				"
				Enabled: the chosen gizmo type is shown and used to control rotation and scaling.\n\
				Disabled: rotation and scaling occurs about the center of the selection bounds.
				"
				.trim(),
			)
			.on_update(move |optional_input: &CheckboxInput| match source {
				PivotToolSource::Select => SelectToolMessage::SelectOptions {
					options: SelectOptionsUpdate::SetPivotGizmoEnabled(optional_input.checked),
				}
				.into(),
				PivotToolSource::Path => PathToolMessage::UpdateOptions {
					options: PathOptionsUpdate::SetPivotGizmoEnabled(optional_input.checked),
				}
				.into(),
			})
			.widget_instance(),
		Separator::new(SeparatorStyle::Related).widget_instance(),
		DropdownInput::new(vec![gizmo_type_entries])
			.selected_index(Some(match state.gizmo_type {
				PivotGizmoType::Pivot => 0,
				PivotGizmoType::Average => 1,
				PivotGizmoType::Active => 2,
			}))
			.tooltip_label("Pivot Gizmo Type")
			.tooltip_description(
				"
				Selects which gizmo type is shown and used as the center of rotation/scaling transformations.\n\
				\n\
				Custom Pivot: rotates and scales relative to the selection bounds, or elsewhere if dragged.\n\
				Origin (Average Point): rotates and scales about the average point of all selected layer origins.\n\
				Origin (Active Object): rotates and scales about the origin of the most recently selected layer.
				"
				.trim(),
			)
			.disabled(!state.enabled)
			.widget_instance(),
	]
}

#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum PivotToolSource {
	Path,
	#[default]
	Select,
}

#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct PivotGizmo {
	pub pivot: Pivot,
	pub state: PivotGizmoState,
	pub layer: Option<LayerNodeIdentifier>,
	pub point: Option<ManipulatorPointId>,
}

impl PivotGizmo {
	pub fn position(&self, document: &DocumentMessageHandler) -> DVec2 {
		let network = &document.network_interface;
		(self.state.enabled)
			.then_some({
				match self.state.gizmo_type {
					PivotGizmoType::Average => Some(network.selected_nodes().selected_visible_and_unlocked_layers_mean_average_origin(network)),
					PivotGizmoType::Pivot => self.pivot.pivot,
					PivotGizmoType::Active => self.layer.map(|layer| graph_modification_utils::get_viewport_origin(layer, network)),
				}
			})
			.flatten()
			.unwrap_or_else(|| self.pivot.transform_from_normalized.transform_point2(DVec2::splat(0.5)))
	}

	pub fn recalculate_transform(&mut self, document: &DocumentMessageHandler) -> DAffine2 {
		self.pivot.recalculate_pivot(document);
		self.pivot.transform_from_normalized
	}

	pub fn pin_active(&self) -> bool {
		self.pivot.pinned && self.state.is_pivot_type()
	}

	pub fn pivot_disconnected(&self) -> bool {
		self.pivot.old_pivot_position == ReferencePoint::None
	}
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize)]
pub enum PivotGizmoType {
	// Pivot
	#[default]
	Pivot,
	// Origin
	Average,
	Active,
	// TODO: Add "Individual"
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Hash, serde::Serialize, serde::Deserialize)]
pub struct PivotGizmoState {
	pub enabled: bool,
	pub gizmo_type: PivotGizmoType,
}

impl PivotGizmoState {
	pub fn is_pivot_type(&self) -> bool {
		// A disabled pivot is considered a pivot-type gizmo that is always centered
		self.gizmo_type == PivotGizmoType::Pivot || !self.enabled
	}

	pub fn is_pivot(&self) -> bool {
		self.gizmo_type == PivotGizmoType::Pivot && self.enabled
	}
}

impl fmt::Display for PivotGizmoType {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			PivotGizmoType::Pivot => write!(f, "Custom Pivot"),
			PivotGizmoType::Average => write!(f, "Origin (Average Point)"),
			PivotGizmoType::Active => write!(f, "Origin (Active Object)"),
			// TODO: Add "Origin (Individual)"
		}
	}
}

#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Pivot {
	/// Pivot between (0,0) and (1,1)
	normalized_pivot: DVec2,
	/// Transform to get from normalized pivot to viewspace
	pub transform_from_normalized: DAffine2,
	/// The viewspace pivot position
	pub pivot: Option<DVec2>,
	/// The old pivot position in the GUI, used to reduce refreshes of the document bar
	pub old_pivot_position: ReferencePoint,
	/// The last ReferencePoint which wasn't none
	pub last_non_none_reference_point: ReferencePoint,
	/// Used to enable and disable the pivot
	pub pinned: bool,
	/// Had selected_visible_and_unlocked_layers
	pub empty: bool,
}

impl Default for Pivot {
	fn default() -> Self {
		Self {
			normalized_pivot: DVec2::splat(0.5),
			transform_from_normalized: Default::default(),
			pivot: Default::default(),
			old_pivot_position: ReferencePoint::Center,
			last_non_none_reference_point: ReferencePoint::Center,
			pinned: false,
			empty: true,
		}
	}
}

impl Pivot {
	/// Recomputes the pivot position and transform.
	pub fn recalculate_pivot(&mut self, document: &DocumentMessageHandler) {
		let selected = document.network_interface.selected_nodes();
		self.empty = !selected.has_selected_nodes();
		if !selected.has_selected_nodes() {
			return;
		}

		let transform = selected
			.selected_visible_and_unlocked_layers(&document.network_interface)
			.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
			.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
			.unwrap_or_default();

		let bounds = document
			.network_interface
			.selected_nodes()
			.selected_visible_and_unlocked_layers(&document.network_interface)
			.filter_map(|layer| {
				if transform.matrix2.determinant().abs() <= f64::EPSILON {
					return None;
				}
				document
					.metadata()
					.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
			})
			.reduce(graphene_std::renderer::Quad::combine_bounds);

		let [min, max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
		self.transform_from_normalized = transform * DAffine2::from_translation(min) * DAffine2::from_scale(max - min);

		if self.old_pivot_position != ReferencePoint::None {
			self.pivot = Some(self.transform_from_normalized.transform_point2(self.normalized_pivot));
		}
	}

	pub fn recalculate_pivot_for_layer(&mut self, document: &DocumentMessageHandler, bounds: Option<[DVec2; 2]>) {
		let selected = document.network_interface.selected_nodes();
		if !selected.has_selected_nodes() {
			self.normalized_pivot = DVec2::splat(0.5);
			self.pivot = None;
			return;
		};

		let [min, max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
		self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
		self.pivot = Some(self.transform_from_normalized.transform_point2(self.normalized_pivot));
	}

	/// Answers if the pivot widget has changed (so we should refresh the tool bar at the top of the canvas).
	pub fn should_refresh_pivot_position(&mut self) -> bool {
		let new = self.to_pivot_position();
		let should_refresh = new != self.old_pivot_position;
		self.old_pivot_position = new;
		should_refresh
	}

	pub fn to_pivot_position(&self) -> ReferencePoint {
		self.normalized_pivot.into()
	}

	/// Sets the viewport position of the pivot.
	pub fn set_viewport_position(&mut self, position: DVec2) {
		if self.transform_from_normalized.matrix2.determinant().abs() <= f64::EPSILON {
			return;
		};

		self.normalized_pivot = self.transform_from_normalized.inverse().transform_point2(position);
		self.pivot = Some(position);
	}

	/// Set the pivot using a normalized position.
	pub fn set_normalized_position(&mut self, position: DVec2) {
		self.normalized_pivot = position;
		self.pivot = Some(self.transform_from_normalized.transform_point2(position));
	}

	/// Answers if the pointer is currently positioned over the pivot.
	pub fn is_over(&self, mouse: DVec2) -> bool {
		self.pivot.filter(|&pivot| mouse.distance_squared(pivot) < (PIVOT_DIAMETER / 2.).powi(2)).is_some()
	}
}
