// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use crate::render::Canvas;
use crate::types::Rect;

use super::TextEdit;
use super::hit_test::{ single_line_align_offset, single_line_scroll_x };
use super::theme;
use super::wrapping::compute_visual_lines;

/// Axis-aligned intersection of two rects. Returns `None` when the rects
/// do not overlap (zero-area / negative-extent results count as
/// "no overlap" so the caller can `filter_map` straight into a clip
/// list).
fn rect_intersect( a: Rect, b: Rect ) -> Option<Rect>
{
	let x0 = a.x.max( b.x );
	let y0 = a.y.max( b.y );
	let x1 = ( a.x + a.width  ).min( b.x + b.width  );
	let y1 = ( a.y + a.height ).min( b.y + b.height );
	if x1 > x0 && y1 > y0
	{
		Some( Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } )
	} else {
		None
	}
}

/// Hit rect for the eye icon at the right edge of a `TextEdit`
/// configured with [`TextEdit::password_toggle`]. Returned in the
/// same coordinate space as the field's `rect`. Pointer / touch
/// dispatch consult this before falling through to cursor placement
/// — a tap inside the zone fires the toggle message instead of
/// moving the caret.
pub fn password_toggle_hit_zone( rect: Rect ) -> Rect
{
	let zone_w = ( theme::PASSWORD_TOGGLE_SIZE
		+ theme::PASSWORD_TOGGLE_SLOP * 2.0
		+ theme::PAD_H * 0.5 ).min( rect.width );
	Rect
	{
		x:      rect.x + rect.width - zone_w,
		y:      rect.y,
		width:  zone_w,
		height: rect.height,
	}
}

impl<Msg: Clone> TextEdit<Msg>
{
	/// Draw the field into `canvas` at `rect`.
	///
	/// `cursor_pos` is the byte offset of the text cursor — supplied by the runtime
	/// from its persistent cursor state rather than from the widget itself.
	/// `selection_anchor` is the other end of the selection range; when
	/// `selection_anchor == cursor_pos` no highlight is painted.
	pub fn draw(
		&self,
		canvas:           &mut Canvas,
		rect:             Rect,
		focused:          bool,
		cursor_pos:       usize,
		selection_anchor: usize,
	)
	{
		if self.is_multiline()
		{
			self.draw_multiline( canvas, rect, focused, cursor_pos, selection_anchor );
			return;
		}
		// Borderless fields skip the surface fill + border stroke
		// entirely. They still get the inner clip + scroll + alignment
		// treatment below, so the field behaves like a regular
		// text input — only the chrome is suppressed.
		if !self.borderless
		{
			let border_c = if focused { theme::focus_border() } else { theme::border() };
			let border_w = if focused { theme::FOCUS_BORDER_W } else { theme::BORDER_W };
			canvas.fill_rect( rect, theme::bg(), theme::RADIUS );
			canvas.stroke_rect( rect, border_c, border_w, theme::RADIUS );
		}

		let font_size = self.effective_font_size( canvas );
		let text_y    = rect.y + (rect.height + font_size) / 2.0 - 2.0;
		let text      = self.display_text();
		let secure    = self.effective_secure();
		// When `password_toggle` is set we reserve a column on the
		// right edge for the eye icon: scroll / align / clip all
		// behave as if the field were narrower so cursor + text
		// never slide under the icon.
		let toggle_reserve = if self.password_toggle.is_some()
		{
			theme::PASSWORD_TOGGLE_SIZE + theme::PASSWORD_TOGGLE_SLOP * 2.0
		}
		else
		{
			0.0
		};
		let text_rect = Rect
		{
			width: ( rect.width - toggle_reserve ).max( theme::PAD_H * 2.0 ),
			..rect
		};
		let scroll_x  = single_line_scroll_x( canvas, text_rect, &self.value, cursor_pos, secure, font_size );
		let align_x   = single_line_align_offset( canvas, text_rect, &self.value, secure, self.align, font_size );

		// Clip text / cursor / selection to the inner band so the
		// scrolled-off portion does not bleed past the pill stroke.
		// Save the parent clip first, intersect with our inner rect,
		// and restore on exit so a `text_edit` wrapped in scroll(...)
		// keeps the parent's clip honoured.
		let outer_clip = canvas.clip_bounds();
		let inner_rect = Rect
		{
			x:      rect.x + theme::PAD_H * 0.5,
			y:      rect.y,
			width:  ( rect.width - theme::PAD_H - toggle_reserve ).max( 0.0 ),
			height: rect.height,
		};
		let composed_clip: Vec<Rect> = if outer_clip.is_empty()
		{
			vec![ inner_rect ]
		} else {
			outer_clip.iter()
				.filter_map( |o| rect_intersect( *o, inner_rect ) )
				.collect()
		};
		canvas.set_clip_rects( &composed_clip );

		// Selection highlight (single-line). Painted before the text
		// so the glyphs sit on top of the tinted band.
		if focused && selection_anchor != cursor_pos
		{
			let ( s, e ) = (
				cursor_pos.min( selection_anchor ).min( self.value.len() ),
				cursor_pos.max( selection_anchor ).min( self.value.len() ),
			);
			let prefix_text = if secure
			{
				"\u{2022}".repeat( self.value[..s].chars().count() )
			} else {
				self.value[..s].to_string()
			};
			let span_text = if secure
			{
				"\u{2022}".repeat( self.value[s..e].chars().count() )
			} else {
				self.value[s..e].to_string()
			};
			let x0 = rect.x + theme::PAD_H + align_x - scroll_x
				+ canvas.measure_text( &prefix_text, font_size );
			let w  = canvas.measure_text( &span_text, font_size );
			let sel_rect = Rect
			{
				x: x0, y: rect.y + 6.0,
				width: w, height: rect.height - 12.0,
			};
			canvas.fill_rect( sel_rect, theme::selection(), 2.0 );
		}

		if text.is_empty()
		{
			// Placeholder follows the same alignment as the value
			// would, so an empty centred / right-aligned field still
			// reads with the placeholder where the digits will land.
			let placeholder_align_x = single_line_align_offset(
				canvas, rect, &self.placeholder, false, self.align, font_size,
			);
			canvas.draw_text(
				&self.placeholder,
				rect.x + theme::PAD_H + placeholder_align_x,
				text_y,
				font_size,
				theme::placeholder(),
			);
		} else {
			canvas.draw_text(
				&text,
				rect.x + theme::PAD_H + align_x - scroll_x,
				text_y,
				font_size,
				theme::text(),
			);
		}

		if focused
		{
			let safe_cursor = cursor_pos.min( self.value.len() );
			let cursor_text = if secure
			{
				"\u{2022}".repeat( self.value[..safe_cursor].chars().count() )
			} else {
				self.value[..safe_cursor].to_string()
			};
			let cursor_x = rect.x + theme::PAD_H + align_x - scroll_x
				+ canvas.measure_text( &cursor_text, font_size );
			// Caret height follows the text line, not the field box — a
			// tall field (launcher pill) otherwise grows a caret twice
			// the glyph height. Matches the multiline caret.
			let cursor_h = font_size + 4.0;
			let cursor_rect = Rect
			{
				x:      cursor_x,
				y:      rect.y + ( rect.height - cursor_h ) / 2.0,
				width:  2.0,
				height: cursor_h,
			};
			canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 );
		}

		// Restore whatever clip was active on entry.
		if outer_clip.is_empty()
		{
			canvas.clear_clip();
		} else {
			canvas.set_clip_rects( &outer_clip );
		}

		// Eye icon for `password_toggle`. Drawn outside the inner
		// clip so it never gets occluded by overflowing text — the
		// preceding clip-restore is the reason this lives here and
		// not earlier in the function. Tinted with the placeholder
		// colour so the icon reads as ambient affordance rather
		// than a primary control.
		if let Some( ( visible, _ ) ) = self.password_toggle.as_ref()
		{
			let icon_name = if *visible { "actions/invisible" } else { "actions/visible" };
			let icon_px   = theme::PASSWORD_TOGGLE_SIZE.round() as u32;
			if let Some( ( rgba, iw, ih ) ) = crate::theme::icon_rgba( icon_name, icon_px )
			{
				let tinted = crate::theme::tint_symbolic( &rgba, theme::placeholder() );
				let zone   = password_toggle_hit_zone( rect );
				let dest   = Rect
				{
					x:      ( zone.x + ( zone.width  - iw as f32 ) / 2.0 ).round(),
					y:      ( rect.y + ( rect.height - ih as f32 ) / 2.0 ).round(),
					width:  iw as f32,
					height: ih as f32,
				};
				canvas.draw_image_data( &tinted, iw, ih, dest, 1.0 );
			}
		}
	}

	fn draw_multiline(
		&self,
		canvas:           &mut Canvas,
		rect:             Rect,
		focused:          bool,
		cursor_pos:       usize,
		selection_anchor: usize,
	)
	{
		let border_c = if focused { theme::focus_border() } else { theme::border() };
		let border_w = if focused { theme::FOCUS_BORDER_W } else { theme::BORDER_W };
		canvas.fill_rect( rect, theme::bg(), theme::RADIUS_MULTI );
		canvas.stroke_rect( rect, border_c, border_w, theme::RADIUS_MULTI );

		let line_h        = theme::FONT_SIZE * theme::LINE_H_MULT;
		let inner_h       = ( rect.height - theme::PAD_V_MULTI * 2.0 ).max( line_h );
		let visible_lines = ( inner_h / line_h ).floor().max( 1.0 ) as usize;
		let inner_width   = ( rect.width - theme::PAD_H * 2.0 ).max( theme::FONT_SIZE );

		// Compute the visual-line layout once per draw. Iterators
		// later go through this list; long logical lines are
		// soft-wrapped at the last whitespace before the row would
		// overflow without mutating the buffer.
		let visual_lines = compute_visual_lines( canvas, &self.value, inner_width, theme::FONT_SIZE );

		// Vertical auto-scroll: keep the cursor's visual line on
		// screen. The cursor sits in the *first* visual line whose
		// `end >= cursor` — at a wrap boundary that places it at the
		// trailing edge of the previous line rather than the start of
		// the next, matching the convention of every standard text
		// editor.
		let safe_cursor       = cursor_pos.min( self.value.len() );
		let cursor_visual_idx = visual_lines.iter()
			.position( |vl| safe_cursor >= vl.start && safe_cursor <= vl.end )
			.unwrap_or( visual_lines.len().saturating_sub( 1 ) );
		let total_lines = visual_lines.len();
		let max_first   = total_lines.saturating_sub( visible_lines );
		let first_line  = if cursor_visual_idx + 1 > visible_lines
		{
			( cursor_visual_idx + 1 - visible_lines ).min( max_first )
		} else { 0 };

		let baseline_0 = rect.y + theme::PAD_V_MULTI + theme::FONT_SIZE;

		// Clip text to the inner rect so partial lines at the bottom
		// stay inside the box and do not bleed onto sibling widgets
		// or the border stroke. Save the parent clip first, then
		// intersect it with our inner rect, so a `scroll(...)`-wrapped
		// multiline edit keeps the parent's clip honoured.
		let outer_clip = canvas.clip_bounds();
		let inner_rect = Rect
		{
			x:      rect.x + theme::PAD_H * 0.5,
			y:      rect.y + theme::PAD_V_MULTI * 0.5,
			width:  ( rect.width  - theme::PAD_H        ).max( 0.0 ),
			height: ( rect.height - theme::PAD_V_MULTI  ).max( 0.0 ),
		};
		let composed_clip: Vec<Rect> = if outer_clip.is_empty()
		{
			vec![ inner_rect ]
		} else {
			outer_clip.iter()
				.filter_map( |o| rect_intersect( *o, inner_rect ) )
				.collect()
		};
		canvas.set_clip_rects( &composed_clip );

		// Selection highlight (multiline). Painted before the text so
		// the glyphs sit on top of the tinted band(s). Iterates
		// visual lines, so a soft-wrapped logical line gets one
		// highlight rect per visual row automatically.
		if focused && selection_anchor != cursor_pos
		{
			let s = cursor_pos.min( selection_anchor ).min( self.value.len() );
			let e = cursor_pos.max( selection_anchor ).min( self.value.len() );
			for ( i, vl ) in visual_lines.iter().enumerate().skip( first_line )
			{
				let visual_idx = i - first_line;
				let y          = rect.y + theme::PAD_V_MULTI + visual_idx as f32 * line_h;
				if y > rect.y + rect.height - theme::PAD_V_MULTI { break; }
				// Intersection of [s, e] with this visual line's
				// byte range. Empty / non-overlapping → skip.
				let span_start = s.max( vl.start );
				let span_end   = e.min( vl.end );
				if span_start >= span_end { continue; }
				let prefix_text = &self.value[ vl.start..span_start ];
				let span_text   = &self.value[ span_start..span_end ];
				let x0 = rect.x + theme::PAD_H + canvas.measure_text( prefix_text, theme::FONT_SIZE );
				let w  = canvas.measure_text( span_text, theme::FONT_SIZE ).max( 4.0 );
				let sel_rect = Rect
				{
					x: x0, y,
					width: w, height: line_h,
				};
				canvas.fill_rect( sel_rect, theme::selection(), 2.0 );
			}
		}

		if self.value.is_empty()
		{
			canvas.draw_text(
				&self.placeholder,
				rect.x + theme::PAD_H,
				baseline_0,
				theme::FONT_SIZE,
				theme::placeholder(),
			);
		} else {
			for ( i, vl ) in visual_lines.iter().enumerate().skip( first_line )
			{
				let visual_idx = i - first_line;
				let y          = baseline_0 + visual_idx as f32 * line_h;
				if y > rect.y + rect.height - theme::PAD_V_MULTI + line_h
				{
					break;
				}
				let line = &self.value[ vl.start..vl.end ];
				canvas.draw_text(
					line,
					rect.x + theme::PAD_H,
					y,
					theme::FONT_SIZE,
					theme::text(),
				);
			}
		}

		if focused
		{
			let vl          = visual_lines[ cursor_visual_idx ];
			let line_prefix = &self.value[ vl.start..safe_cursor ];
			let cursor_x    = rect.x + theme::PAD_H
				+ canvas.measure_text( line_prefix, theme::FONT_SIZE );
			let visual_y_idx = cursor_visual_idx.saturating_sub( first_line );
			let cursor_y    = rect.y + theme::PAD_V_MULTI + visual_y_idx as f32 * line_h;
			let cursor_rect = Rect
			{
				x:      cursor_x,
				y:      cursor_y + 2.0,
				width:  2.0,
				height: theme::FONT_SIZE + 4.0,
			};
			canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 );
		}

		// Restore whatever clip was active on entry — a single empty
		// `Vec` from `clip_bounds` means "no clip", which we model by
		// calling `clear_clip` instead of pushing an empty slice.
		if outer_clip.is_empty()
		{
			canvas.clear_clip();
		} else {
			canvas.set_clip_rects( &outer_clip );
		}
	}
}
