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

//! Element-tree recursive walker.
//!
//! [`layout_and_draw`] is the single bottom of the draw pipeline:
//! given an [`Element`] + its allocated rect, it lays out children,
//! paints leaves, threads scroll sub-canvases, and records the
//! [`LaidOutWidget`] entries the input layer will hit-test against.
//! Both software and GPU paths funnel every widget through this one
//! function.

use crate::render::Canvas;
use crate::types::Rect;
use crate::widget::{ Element, LaidOutWidget, WidgetHandlers };

use super::DrawCtx;
use super::damage::clamp_rect_to;

pub( crate ) fn layout_and_draw<Msg: Clone>(
	element:  &Element<Msg>,
	canvas:   &mut Canvas,
	rect:     Rect,
	ctx:      &mut DrawCtx<Msg>,
	flat_idx: usize,
) -> usize
{
	match element
	{
		Element::Column( col ) =>
		{
			let child_rects = col.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &col.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Row( r ) =>
		{
			let child_rects = r.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &r.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Stack( s ) =>
		{
			let child_rects = s.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				let child = &s.children[ child_i ];
				// A placed child may carry a clip rect (Android clipChildren): clip the
				// canvas to it for the child's subtree, then restore the prior clip.
				// The clip is in the Stack's coordinate space (like the placed rect),
				// so it is shifted by the Stack's origin to match — otherwise a Stack
				// laid out at a non-zero origin clips in the wrong place.
				if let Some( clip ) = child.7
				{
					let saved = canvas.clip_bounds();
					let clip = Rect { x: rect.x + clip.x, y: rect.y + clip.y, width: clip.width, height: clip.height };
					canvas.set_clip_rects( &[ clip ] );
					idx = layout_and_draw::<Msg>( &child.0, canvas, child_rect, ctx, idx );
					canvas.set_clip_rects( &saved );
				}
				else
				{
					idx = layout_and_draw::<Msg>( &child.0, canvas, child_rect, ctx, idx );
				}
			}
			idx
		}
		Element::WrapGrid( g ) =>
		{
			let child_rects = g.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &g.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Carousel( car ) =>
		{
			let child_rects = car.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &car.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Flex( f ) =>
		{
			// `Flex` is invisible chrome: it claimed leftover width up at
			// `Row::layout`, here we just unwrap it and draw the child
			// inside the allocated rect. No flat-index of its own.
			layout_and_draw::<Msg>( f.child.as_ref(), canvas, rect, ctx, flat_idx )
		}
		Element::AnchoredOverlay( a ) =>
		{
			// Look up the anchor's rect from the previous frame's
			// `widget_rects` snapshot. If found, place the child flush
			// below the anchor at the child's intrinsic size; if not,
			// fall back to the parent-supplied rect so the child still
			// renders (modal-style, typically a one-frame artefact on
			// the first frame after open).
			let anchor_rect = ctx.previous_widget_rects.iter()
				.find( |w| w.id == Some( a.anchor_id ) )
				.map( |w| w.rect );
			let target = match anchor_rect
			{
				Some( anchor ) =>
				{
					// Use the child's intrinsic preferred size so the
					// popup keeps its design width / height regardless
					// of how wide the trigger pill happens to be.
					let ( w, h ) = a.child.preferred_size( rect.width, canvas );
					Rect
					{
						x:      anchor.x,
						y:      anchor.y + anchor.height + a.gap,
						width:  w,
						height: h,
					}
				}
				None => rect,
			};
			layout_and_draw::<Msg>( a.child.as_ref(), canvas, target, ctx, flat_idx )
		}
		Element::Pressable( p ) =>
		{
			// Push the wrapper's hit rect *before* recursing so that any
			// interactive child pushed during recursion sits later in
			// `widget_rects` and wins under `iter().rev()` hit testing.
			let my_idx = flat_idx;
			if p.has_handler()
			{
				ctx.widget_rects.push( LaidOutWidget
				{
					rect,
					flat_idx:           my_idx,
					id:                 p.id,
					paint_rect:         rect,
					handlers:           WidgetHandlers::Button
					{
						on_press:      p.on_press.clone(),
						on_long_press: p.on_long_press.clone(),
						on_drag_start: p.on_drag_start.clone(),
						on_escape:     p.on_escape.clone(),
						repeating:     false,
					},
					keyboard_focusable: false,
					cursor:             p.cursor.unwrap_or( crate::types::CursorShape::Pointer ),
					tooltip:            None,
					accessible_label:   None,
					is_live_region:     ctx.live_depth > 0,
				} );
			}
			layout_and_draw::<Msg>( p.child.as_ref(), canvas, rect, ctx, my_idx + 1 )
		}
		Element::Container( c ) =>
		{
			let saved_alpha = canvas.global_alpha();
			canvas.set_global_alpha( saved_alpha * c.opacity );
			let live_inc = if c.a11y_live { 1 } else { 0 };
			ctx.live_depth += live_inc;

			let rect = if let Some( mw ) = c.max_width
			{
				crate::types::Rect { width: rect.width.min( mw ), ..rect }
			} else {
				rect
			};

			// Surface slot takes precedence over flat background; falls
			// through to `c.background` when the slot is absent (third-
			// party theme without the named surface — content still
			// renders, just without the themed chrome).
			let painted = match c.surface.as_deref()
			{
				Some( slot ) => match crate::theme::resolve_surface( slot )
				{
					Some( ( surf, outer ) ) =>
					{
						canvas.fill_surface
						(
							rect,
							&surf.fill,
							&outer,
							&surf.inset_shadows,
							c.corners,
						);
						true
					}
					None => false,
				},
				None => false,
			};
			if !painted
			{
				if let Some( ref bg ) = c.background
				{
					canvas.fill_paint_rect( rect, bg, c.corners );
				}
			}
			if let Some( ( color, width ) ) = c.border
			{
				canvas.stroke_rect( rect, color, width, c.corners );
			}
			let pad_l = canvas.resolve_geom( c.pad_left );
			let pad_r = canvas.resolve_geom( c.pad_right );
			let pad_t = canvas.resolve_geom( c.pad_top );
			let pad_b = canvas.resolve_geom( c.pad_bottom );
			let inner = crate::types::Rect
			{
				x:      rect.x + pad_l,
				y:      rect.y + pad_t,
				width:  ( rect.width  - pad_l - pad_r ).max( 0.0 ),
				height: ( rect.height - pad_t - pad_b ).max( 0.0 ),
			};
			let result = layout_and_draw::<Msg>( c.child.as_ref(), canvas, inner, ctx, flat_idx );

			ctx.live_depth -= live_inc;
			canvas.set_global_alpha( saved_alpha );
			result
		}
		Element::Scroll( s ) =>
		{
			let my_idx     = flat_idx;
			let axis       = s.axis;
			let ( ox_raw, oy_raw ) = ctx.scroll_offsets.get( &my_idx ).copied().unwrap_or( ( 0.0, 0.0 ) );
			// Width-aware preferred size: when the axis allows horizontal
			// overflow we let the child report its natural width by
			// asking with `f32::INFINITY`; otherwise the child stays
			// clamped to the viewport so wraps still happen.
			let child_w_max = if axis.allows_x() { f32::INFINITY } else { rect.width };
			let ( child_w, child_h ) = s.child.preferred_size( child_w_max, canvas );
			let ox = if axis.allows_x() { crate::widget::scroll::clamp_offset( ox_raw, child_w, rect.width  ) } else { 0.0 };
			let oy = if axis.allows_y() { crate::widget::scroll::clamp_offset( oy_raw, child_h, rect.height ) } else { 0.0 };
			// Write the clamped offsets back so input handlers (wheel /
			// drag) cannot accumulate past the content extents. Without
			// this, repeated wheel ticks past the edge keep growing
			// `*_raw` and the user has to "undo" that excess before the
			// content scrolls back. The clamp is idempotent for in-range
			// values, so the only effect is collapsing out-of-range
			// entries to the legitimate maximum.
			if ox != ox_raw || oy != oy_raw
			{
				ctx.scroll_offsets.insert( my_idx, ( ox, oy ) );
			}

			// Reuse the sub-canvas from the previous frame if its size matches;
			// only reallocate when the viewport is resized.
			let sw  = (rect.width.ceil()  as u32).max( 1 );
			let sh  = (rect.height.ceil() as u32).max( 1 );
			let mut sub = ctx.scroll_canvases.remove( &my_idx )
				.filter( |c| c.size() == ( sw, sh ) )
				.unwrap_or_else( || canvas.sub_canvas( sw, sh ) );
			sub.clear();

			// Child content fills at least the full viewport on each
			// allowed axis so that children with `Bottom`/`Right`
			// alignment are positioned correctly when the content is
			// shorter than the viewport along that axis.
			let effective_w = if axis.allows_x() { child_w.max( rect.width  ) } else { rect.width  };
			let effective_h = if axis.allows_y() { child_h.max( rect.height ) } else { rect.height };
			// Shift child by `(-ox, -oy)` so scrolled content appears at
			// `(0, 0)` of the sub-canvas.
			let child_rect = Rect { x: -ox, y: -oy, width: effective_w, height: effective_h };

			let rects_before        = ctx.widget_rects.len();
			let scroll_rects_before = ctx.scroll_rects.len();
			let next_idx = layout_and_draw::<Msg>( s.child.as_ref(), &mut sub, child_rect, ctx, my_idx + 1 );

			// Translate widget_rects from sub-canvas space to global space; drop
			// clipped ones. Also clamp `paint_rect` to the scroll viewport so a
			// widget painting outside the sub-canvas does not invalidate pixels
			// it cannot actually reach (the sub-canvas is blitted as a whole,
			// so nothing outside the viewport appears on screen anyway).
			let new_rects: Vec<LaidOutWidget<Msg>> = ctx.widget_rects.drain( rects_before.. ).collect();

			// Capture every interactive item the child laid out — including
			// items that the visibility filter below will discard — so the
			// keyboard handler can step `hovered_idx` item-by-item without
			// caring about which items are currently scrolled into view.
			// The recorded Y is in pre-translation, pre-offset coordinates
			// (i.e. `child_y` relative to the start of the scroll content),
			// recovered by undoing the `-oy` shift the child layout pass
			// applied: `content_y = w.rect.y - child_rect.y = w.rect.y + oy`.
			let navigable: Vec<( usize, f32, f32 )> = new_rects.iter()
				.filter( |w| w.handlers.is_navigable_list_item() )
				.map( |w| ( w.flat_idx, w.rect.y + oy, w.rect.height ) )
				.collect();
			if !navigable.is_empty()
			{
				ctx.scroll_navigable_items.insert( my_idx, navigable );
			}

			for mut w in new_rects
			{
				// Sub-canvas origin (0,0) maps to global (rect.x, rect.y).
				w.rect.x += rect.x;
				w.rect.y += rect.y;
				w.paint_rect.x += rect.x;
				w.paint_rect.y += rect.y;
				w.paint_rect = clamp_rect_to( w.paint_rect, rect );
				// Only keep widgets at least partially visible inside the
				// viewport — now bi-axial because Scroll::horizontal /
				// Scroll::both push content off-screen along x too.
				let visible_y = w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height;
				let visible_x = w.rect.x + w.rect.width  > rect.x && w.rect.x < rect.x + rect.width;
				if visible_x && visible_y
				{
					ctx.widget_rects.push( w );
				}
			}

			// Same translation for scroll_rects pushed by any nested
			// scroll widgets — without this, the wheel handler hit-tests
			// in surface coords against rects in sub-canvas coords and
			// only matches when the sub-canvas happens to start at
			// (0, 0) of the surface. Clamp to the outer scroll rect so
			// the inner scroll only captures wheel events inside its
			// visible region.
			let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> =
				ctx.scroll_rects.drain( scroll_rects_before.. ).collect();
			for ( mut r, idx, ax ) in new_scroll_rects
			{
				r.x += rect.x;
				r.y += rect.y;
				let clamped = clamp_rect_to( r, rect );
				if clamped.width > 0.0 && clamped.height > 0.0
				{
					ctx.scroll_rects.push( ( clamped, idx, ax ) );
				}
			}

			ctx.scroll_rects.push( ( rect, my_idx, axis ) );

			canvas.blit( &sub, rect.x as i32, rect.y as i32 );

			ctx.scroll_canvases.insert( my_idx, sub );

			next_idx
		}
		Element::Viewport( v ) =>
		{
			let child_h = v.child.preferred_size( rect.width, canvas ).1;
			let effective_h = child_h.max( rect.height );
			let vw = ( rect.width.ceil() as u32 ).max( 1 );
			let vh = ( rect.height.ceil() as u32 ).max( 1 );
			let mut sub = canvas.sub_canvas( vw, vh );
			if v.local_viewport { sub.set_local_layout_viewport(); }
			sub.clear();

			let child_rect = Rect { x: 0.0, y: 0.0, width: rect.width, height: effective_h };
			let rects_before        = ctx.widget_rects.len();
			let scroll_rects_before = ctx.scroll_rects.len();
			let next_idx = layout_and_draw::<Msg>( v.child.as_ref(), &mut sub, child_rect, ctx, flat_idx );

			let new_rects: Vec<LaidOutWidget<Msg>> = ctx.widget_rects.drain( rects_before.. ).collect();
			for mut w in new_rects
			{
				w.rect.x += rect.x;
				w.rect.y += rect.y;
				w.paint_rect.x += rect.x;
				w.paint_rect.y += rect.y;
				w.paint_rect = clamp_rect_to( w.paint_rect, rect );
				if w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height
				{
					ctx.widget_rects.push( w );
				}
			}

			// Translate scroll_rects pushed by nested scroll widgets
			// from sub-canvas to surface space. Same reasoning as the
			// scroll-inside-scroll case above.
			let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> =
				ctx.scroll_rects.drain( scroll_rects_before.. ).collect();
			for ( mut r, idx, ax ) in new_scroll_rects
			{
				r.x += rect.x;
				r.y += rect.y;
				let clamped = clamp_rect_to( r, rect );
				if clamped.width > 0.0 && clamped.height > 0.0
				{
					ctx.scroll_rects.push( ( clamped, idx, ax ) );
				}
			}

			canvas.blit_fade_bottom( &sub, rect.x as i32, rect.y as i32, v.fade_bottom );
			next_idx
		}
		Element::RichText( r ) =>
		{
			r.draw( canvas, rect );
			// One hit rect per link line, each its own flat_idx so hover/press
			// state does not bleed between links sharing a paragraph.
			let link_rects = r.link_rects( rect, canvas );
			let consumed = link_rects.len().max( 1 );
			for ( k, ( lr, msg ) ) in link_rects.into_iter().enumerate()
			{
				ctx.widget_rects.push( LaidOutWidget
				{
					rect:               lr,
					flat_idx:           flat_idx + k,
					id:                 None,
					paint_rect:         lr,
					handlers:           WidgetHandlers::Button
					{
						on_press:      Some( msg ),
						on_long_press: None,
						on_drag_start: None,
						on_escape:     None,
						repeating:     false,
					},
					keyboard_focusable: false,
					cursor:             crate::types::CursorShape::Pointer,
					tooltip:            None,
					accessible_label:   None,
					is_live_region:     ctx.live_depth > 0,
				} );
			}
			flat_idx + consumed
		}
		other =>
		{
			// Gate the focus ring by `is_focusable`: a widget that opts out
			// of keyboard focus (e.g. `Button::focusable(false)`) should not
			// paint a ring even if it ends up in `focused_idx` after a tap.
			let is_focused  = ctx.focused_idx == Some( flat_idx ) && other.is_focusable();
			let is_hovered  = ctx.hovered_idx == Some( flat_idx );
			let is_pressed  = ctx.pressed_idx == Some( flat_idx );
			let cursor_pos  = ctx.cursor_state.get( &flat_idx ).copied().unwrap_or( 0 );
			let sel_anchor  = ctx.selection_anchor.get( &flat_idx ).copied().unwrap_or( cursor_pos );
			let widget_id   = match other
			{
				Element::Button( b )   => b.id,
				Element::TextEdit( t ) => t.id,
				Element::Toggle( t )   => t.id,
				Element::Checkbox( c ) => c.id,
				Element::Radio( r )    => r.id,
				Element::ListItem( l ) => l.id,
				Element::WindowButton( b ) => b.id,
				_                      => None,
			};
			other.draw( canvas, rect, is_focused, is_hovered, is_pressed, cursor_pos, sel_anchor );
			if other.is_interactive()
			{
				let mut handlers = other.handlers();
				// Horizontal sliders resolve their thumb size through the
				// widget-scaling mode here — layout has the canvas — so the
				// input path maps clicks against the same thumb the renderer
				// drew (see `slider::value_from_x_in_rect`).
				if matches!( other, Element::Slider( _ ) )
				{
					handlers.set_slider_thumb_px( crate::widget::slider::resolved_thumb_px( canvas ) );
				}
				ctx.widget_rects.push( LaidOutWidget
				{
					rect,
					flat_idx,
					id:                 widget_id,
					paint_rect:         other.paint_bounds( rect ),
					handlers,
					keyboard_focusable: other.is_focusable(),
					cursor:             other.cursor_shape(),
					tooltip:            other.tooltip().map( str::to_string ),
					accessible_label:   other.accessible_label(),
					is_live_region:     ctx.live_depth > 0,
				} );
			}
			else
			{
				let live = ctx.live_depth > 0;
				match other
				{
					Element::Text( t ) if !t.content.is_empty() =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: Some( t.content.clone() ), live,
							kind: crate::a11y::tree::AccessibleExtraKind::Label,
						} );
					}
					Element::Image( _ ) =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: None, live,
							kind: crate::a11y::tree::AccessibleExtraKind::Image,
						} );
					}
					Element::Separator( _ ) =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: None, live,
							kind: crate::a11y::tree::AccessibleExtraKind::Separator,
						} );
					}
					Element::ProgressBar( p ) =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: None, live,
							kind: crate::a11y::tree::AccessibleExtraKind::Progress( p.value ),
						} );
					}
					_ => {}
				}
			}
			flat_idx + 1
		}
	}
}
