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

use crate::render::Canvas;
use crate::types::{ Length, Point, Rect };

use super::theme;
use super::wrapping::compute_visual_lines;

/// Convert a pointer position inside the widget rect to the byte offset
/// in `value` the cursor should land on. Free function so the runtime
/// can call it with the value snapshot it already holds in
/// [`crate::widget::WidgetHandlers::TextEdit`] without re-walking the
/// element tree to find the original [`super::TextEdit`] struct.
///
/// `multiline` and `secure` mirror the matching fields on the source
/// widget; `cursor_pos` is the current cursor — needed by the
/// multiline branch to reproduce the same vertical scroll the most
/// recent [`super::TextEdit::draw`] call computed.
pub( crate ) fn byte_offset_at(
	canvas:     &Canvas,
	rect:       Rect,
	pos:        Point,
	value:      &str,
	multiline:  bool,
	secure:     bool,
	cursor_pos: usize,
	align:      crate::widget::text::TextAlign,
	font_size:  Option<Length>,
) -> usize
{
	if value.is_empty() { return 0; }

	if multiline && !secure
	{
		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 );

		// Recompute the visual layout from current state. Same call
		// as `draw_multiline` makes, so the click maps to the line /
		// column the user actually sees.
		let visual_lines      = compute_visual_lines( canvas, value, inner_width, theme::FONT_SIZE );
		let safe_cursor       = cursor_pos.min( 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 visual_idx  = ( ( pos.y - rect.y - theme::PAD_V_MULTI ) / line_h )
			.max( 0.0 ) as usize;
		let target_idx  = ( first_line + visual_idx ).min( total_lines.saturating_sub( 1 ) );
		let vl          = visual_lines[ target_idx ];

		byte_offset_in_line( canvas, value, vl.start, vl.end, pos.x - rect.x - theme::PAD_H, false, theme::FONT_SIZE )
	} else {
		// Resolve the font-size sentinel (0.0 / negative → widget-scaling mode)
		// so a click maps against the same size the renderer drew with.
		let font_size = super::resolve_font_size( canvas, font_size );
		// Mirror the horizontal scroll *and* alignment the renderer
		// applies so a click lands on the glyph the user actually
		// sees, not on the byte offset that would correspond to an
		// unscrolled, left-aligned layout.
		let scroll_x = single_line_scroll_x( canvas, rect, value, cursor_pos, secure, font_size );
		let align_x  = single_line_align_offset( canvas, rect, value, secure, align, font_size );
		byte_offset_in_line(
			canvas, value, 0, value.len(),
			pos.x - rect.x - theme::PAD_H - align_x + scroll_x,
			secure,
			font_size,
		)
	}
}

/// Horizontal scroll offset, in pixels, applied to a single-line
/// `text_edit` so the cursor stays visible inside the inner rect when
/// the value is wider than the box. Stateless — derived purely from
/// the current cursor position. Three regimes:
///
/// * cursor in the **left half** of the visible band → no scroll, the
///   start of the text reads naturally;
/// * cursor in the **right half from the end** → anchor to end so the
///   tail of the value is in view (this is the "typing past the right
///   edge" case the user sees the most);
/// * cursor anywhere else → centre the cursor in the visible band so
///   navigation with the arrow keys keeps trailing context on both
///   sides instead of jumping the text on every keystroke.
///
/// `secure` toggles bullet substitution before measuring so a
/// password field scrolls based on the displayed bullets, not the
/// underlying value's glyph widths.
pub( crate ) fn single_line_scroll_x(
	canvas:    &Canvas,
	rect:      Rect,
	value:     &str,
	cursor:    usize,
	secure:    bool,
	font_size: f32,
) -> f32
{
	let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( font_size );
	let safe_cursor = cursor.min( value.len() );

	let display_value: String = if secure
	{
		"\u{2022}".repeat( value.chars().count() )
	} else {
		value.to_string()
	};
	let prefix_text: String = if secure
	{
		"\u{2022}".repeat( value[..safe_cursor].chars().count() )
	} else {
		value[..safe_cursor].to_string()
	};

	let total_w  = canvas.measure_text( &display_value, font_size );
	if total_w <= inner_width { return 0.0; }
	let prefix_w = canvas.measure_text( &prefix_text,   font_size );
	let suffix_w = ( total_w - prefix_w ).max( 0.0 );
	const MARGIN: f32 = 4.0;

	if prefix_w <= inner_width / 2.0
	{
		0.0
	} else if suffix_w <= inner_width / 2.0 {
		( total_w - inner_width + MARGIN ).max( 0.0 )
	} else {
		prefix_w - inner_width / 2.0
	}
}

/// Horizontal offset, in pixels, applied on top of `single_line_scroll_x`
/// to honour the field's [`crate::widget::text::TextAlign`]. Only meaningful
/// while the value still fits inside `inner_width`; once the text
/// overflows, scrolling owns the layout and the alignment offset
/// collapses to `0` so anchoring the cursor / end of text reads
/// naturally without fighting the alignment.
pub( crate ) fn single_line_align_offset(
	canvas:    &Canvas,
	rect:      Rect,
	value:     &str,
	secure:    bool,
	align:     crate::widget::text::TextAlign,
	font_size: f32,
) -> f32
{
	use crate::widget::text::TextAlign;
	if matches!( align, TextAlign::Left ) { return 0.0; }
	let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( font_size );
	let display_value: String = if secure
	{
		"\u{2022}".repeat( value.chars().count() )
	} else {
		value.to_string()
	};
	let total_w = canvas.measure_text( &display_value, font_size );
	if total_w >= inner_width { return 0.0; }
	match align
	{
		TextAlign::Left   => 0.0,
		TextAlign::Center => ( inner_width - total_w ) * 0.5,
		TextAlign::Right  => inner_width - total_w,
	}
}

/// Walk the slice `value[line_start..line_end]` char by char,
/// accumulating widths, and return the byte offset (within `value`)
/// where the click `target_x` falls. The "snap to nearest boundary"
/// rule is the standard `text input is text input` behaviour: if the
/// click is past the half-glyph mark, the cursor lands *after* the
/// glyph. `secure` toggles bullet substitution at measurement time so
/// the same logic works for password fields.
pub( super ) fn byte_offset_in_line(
	canvas:     &Canvas,
	value:      &str,
	line_start: usize,
	line_end:   usize,
	target_x:   f32,
	secure:     bool,
	font_size:  f32,
) -> usize
{
	let line = &value[line_start..line_end];
	if target_x <= 0.0 { return line_start; }
	let mut acc_w = 0.0f32;
	let mut last_byte = line_start;
	for ( ofs, ch ) in line.char_indices()
	{
		let glyph_str = if secure { "\u{2022}".to_string() } else { ch.to_string() };
		let w         = canvas.measure_text( &glyph_str, font_size );
		if target_x < acc_w + w * 0.5
		{
			return line_start + ofs;
		}
		acc_w     += w;
		last_byte  = line_start + ofs + ch.len_utf8();
	}
	last_byte.min( line_end )
}
