#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::tabs_in_doc_comments, clippy::doc_lazy_continuation)]
//! # egui_gradients_core
//!
//! EGUI Gradients Core is a byte-sized crate which serves as the base for all `egui_x_gradients`.
//!
//! ## Getting Started
//! This crate is better used by the actual crates;
//! Such as the [`egui_colorimage_gradients`](https://crates.io/crates/egui_colorimage_gradients) crate
//! and [`egui_border_gradients`](https://crates.io/crates/egui_border_gradients) crate (soon to be released!)
//!
//! Note: This crate MIGHT have an API change; To be more general purposed & move the
//! egui wrapping logic to their own crates.
//!
//! Note: Thank you for using my crate!
//! If you have any issues or queries, you can reach out to me at
//! [Issues](https://github.com/Perzium/egui_gradients_core/issues) or
//! [Discussions](https://github.com/Perzium/egui_gradients_core/discussions).

// Just a bunch of helpers
mod helpers;

// Enums are included within this file; Or that's how it should seem?
mod enums;

// Use whichever one based on features
#[cfg(feature = "below_egui_0_34_0")]
pub use crate::enums::Direction;
pub use crate::{
	enums::{GradientAngle, Position, Shape},
	helpers::FromArray,
};
#[cfg(not(feature = "below_egui_0_34_0"))]
pub use egui::Direction;

// Use for simplification
use egui::{Color32, ColorImage};
use std::f32::consts::{FRAC_PI_2, PI};

use crate::helpers::{
	calculate_angle, calculate_position, resolve_color_stops, sample_resolved_gradient,
};

/// ## Conic Gradient
/// Generates a [`ColorImage`] with a conic gradient
///
/// ### Parameters
/// * Size `[width, height]`: Determines the size of the ColorImage generated
/// * Angle `Some(GradientAngle)`: Determines the angle of the Conic gradient.
/// 	Leave it at `None` for an ellipse.
/// * Position `Some(Position)`: Determines where the Conic gradient starts.
/// 	Leave it at `None` for center.
/// * Colors `&[(Color32, Option<u8>)]`: A list of colors with the percent of the image they take up.
/// 	Leave it at `None` for uniform distribution.
pub fn conic_gradient(
	size: [usize; 2],
	angle: Option<GradientAngle>,
	position: Option<Position>,
	colors: &[(Color32, Option<u8>)],
) -> ColorImage {
	// Fetch and store the size of image
	let [w, h] = size;
	let source_size = egui::vec2(w as f32, h as f32);

	// Return empty image if no colors were provided
	if colors.is_empty() {
		return ColorImage { size, source_size, pixels: Vec::new() };
	}

	// Resolved pixels and a pixel buffer
	let resolved = resolve_color_stops(colors);
	let mut pixels = Vec::with_capacity(w * h);

	// Resolve position
	let (cx, cy) = calculate_position(position);

	// Resolve angle offset
	let angle_offset =
		if let Some(gradient_angle) = angle { calculate_angle(gradient_angle) } else { 0.0 }; // results to GradientAngle::Degree(0.0)

	// Loop through height
	for y in 0..h {
		let ny = if h > 1 { y as f32 / (h - 1) as f32 } else { 0.0 };
		let dy = ny - cy;

		// Loop through width
		for x in 0..w {
			let nx = if w > 1 { x as f32 / (w - 1) as f32 } else { 0.0 };
			let dx = nx - cx;

			// Calculate angle, apply starting offset, and normalize
			let pixel_angle = dy.atan2(dx);
			let mut t = (pixel_angle - angle_offset + PI) / (2.0 * PI);

			// Wrap around mapped value to keep it strictly between 0 and 1
			t = t.fract();
			if t < 0.0 {
				t += 1.0;
			}

			pixels.push(sample_resolved_gradient(t, &resolved));
		}
	}

	ColorImage { size, source_size, pixels }
}

/// ## Linear Gradient
/// Generates a [`ColorImage`] with a linear gradient
///
/// ### Parameters
/// * Size `[width, height]`: Determines the size of the ColorImage generated.
/// * Gradient Angle `GradientAngle`: Determines the rotation of the gradient.
/// * Colors `&[(Color32, Option<u8>)]`: A list of colors with the percent of the image they take up.
/// 	Leave it at `None` for uniform distribution.
pub fn linear_gradient(
	size: [usize; 2],
	gradient_angle: GradientAngle,
	colors: &[(Color32, Option<u8>)],
) -> ColorImage {
	// Fetch and store the size of image
	let [w, h] = size;
	let source_size = egui::vec2(w as f32, h as f32);

	// Return empty image if no colors were provided
	if colors.is_empty() {
		return ColorImage { size, source_size, pixels: Vec::new() };
	}

	// Angle in which the gradient is rotated
	let theta = calculate_angle(gradient_angle);

	// Functions and stretches
	let (sin, cos) = theta.sin_cos();
	let stretch = (sin.abs() + cos.abs()) * 0.5;

	// Calculate resolved color stops
	let resolved = resolve_color_stops(colors);

	// Create a pixel buffer
	let mut pixels = Vec::with_capacity(w * h);

	// Loop through height
	for y in 0..h {
		let ny = if h > 1 { y as f32 / ((h - 1) as f32).max(1.0) } else { 0.0 };
		let dy = ny - 0.5;

		// Loop through width
		for x in 0..w {
			let nx = if w > 1 { x as f32 / (w - 1) as f32 } else { 0.0 };
			let dx = nx - 0.5;

			// Dot product projection
			let projection = dx * cos + dy * sin;

			// Find factor
			#[rustfmt::skip]
			let t =
			if stretch > 0.0 { ((projection / stretch) * 0.5 + 0.5).clamp(0.0, 1.0) }
			else { 0.5 };

			// Sample interpolated color and push
			let final_color = sample_resolved_gradient(t, &resolved);
			pixels.push(final_color);
		}
	}

	ColorImage { size, source_size, pixels }
}

/// ## Radial Gradient
/// Generates a [`ColorImage`] with a radial gradient
///
/// ### Parameters
/// * Size `[width, height]`: Determines the size of the ColorImage generated
/// * Shape `Some(Shape)`: Determines the shape of the Radial gradient.
/// 	Leave it at `None` for an ellipse.
/// * Position `Some(Position)`: Determines where the Radial gradient starts.
/// 	Leave it at `None` for center.
/// * Colors `&[(Color32, Option<u8>)]`: A list of colors with the percent of the image they take up.
/// 	Leave it at `None` for uniform distribution.
pub fn radial_gradient(
	size: [usize; 2],
	shape: Option<Shape>,
	position: Option<Position>,
	colors: &[(Color32, Option<u8>)],
) -> ColorImage {
	// Fetch and store the size of image
	let [w, h] = size;
	let source_size = egui::vec2(w as f32, h as f32);

	// Return empty image if no colors were provided
	if colors.is_empty() {
		return ColorImage { size, source_size, pixels: Vec::new() };
	}

	// Resolved pixels and a pixel buffer
	let resolved = resolve_color_stops(colors);
	let mut pixels = Vec::with_capacity(w * h);

	// Resolve position to normalized coordinates (0 to 1)
	let (cx, cy) = calculate_position(position);

	let is_circle = matches!(shape, Some(Shape::Circle));
	let aspect_ratio = if is_circle && h > 0 { w as f32 / h as f32 } else { 1.0 };

	// Loop through height
	for y in 0..h {
		let ny = if h > 1 { y as f32 / (h - 1) as f32 } else { 0.0 };
		let dy = ny - cy;

		// Loop through width
		for x in 0..w {
			let nx = if w > 1 { x as f32 / (w - 1) as f32 } else { 0.0 };
			let mut dx = nx - cx;

			// Adjust aspect ratio for perfect circles
			if is_circle {
				dx *= aspect_ratio;
			}

			// Pythagorean theorem scaled by 2 to stretch across the bounds
			let t = ((dx * dx + dy * dy).sqrt() * 2.0).clamp(0.0, 1.0);
			pixels.push(sample_resolved_gradient(t, &resolved));
		}
	}

	ColorImage { size, source_size, pixels }
}
