<?php
/**
 * Server-side render for blocksify/tabs.
 *
 * Walks `$block->parsed_block['innerBlocks']` (the Tab children) once
 * to build BOTH the tablist nav strip and the tabpanel divs in a
 * single pass. Per-tab metadata (label, subtitle, icon, anchor) is
 * read from the Tabs's own `titles[]` attribute, indexed positionally
 * to match the child order — all metadata centralized on the parent
 * keeps `tabCount` and `titles.length` in lockstep through a single
 * atomic `setAttributes` per editor handler.
 *
 * Tab's individual render.php is short-circuited via `pre_render_block`
 * filter (see `blocksify_skip_tab_render` in blocksify.php), so we
 * call `render_block()` only on each Tab's own innerBlocks (the user
 * content) — never on the Tab itself, which would otherwise produce
 * its own wrapper output that we'd ignore + waste CPU on.
 *
 * Per-instance CSS for the tabs (root + tablist/tab/tabActive parts +
 * icon-position rules) flows through `Blocksify_CSS_Pipeline` once
 * the adapter map registers `blocksify/tabs` (see
 * `Blocksify_CSS_Generator::generate_block_css`).
 *
 * @package Blocksify
 *
 * @var array    $attributes Block attributes.
 * @var string   $content    Saved block markup (ignored — we walk
 *                            parsed_block instead).
 * @var WP_Block $block      Parsed block instance.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$uid           = isset( $attributes['uid'] ) ? (string) $attributes['uid'] : '';
$preset        = isset( $attributes['preset'] ) ? (string) $attributes['preset'] : 'default';
// Tab Position — where the tablist sits relative to the panels.
// Whitelist clamps to the 4 supported values so user-provided attr
// can't inject arbitrary class fragments.
$tab_position  = isset( $attributes['tabPosition'] ) ? (string) $attributes['tabPosition'] : 'top';
if ( ! in_array( $tab_position, array( 'top', 'right', 'bottom', 'left' ), true ) ) {
	$tab_position = 'top';
}
// Tab distribution (tabWidth) + alignment + overflow + per-device position
// / width / gap are all emitted as per-instance CSS by the CSS generator
// (`tabs_layout_css`), NOT as wrapper classes — so they can be responsive.
// The `is-position-{desktop}` class below stays purely as a marker /
// hook (aria-orientation derives from `$tab_position` directly).
// aria-orientation derives from $tab_position: top/bottom → horizontal
// (tabs sit in a row); left/right → vertical (tabs sit in a column).
// view.js reads this attribute to switch keyboard arrow keys between
// Left/Right and Up/Down.
$aria_orientation = ( 'left' === $tab_position || 'right' === $tab_position ) ? 'vertical' : 'horizontal';
// Frontend uses `defaultActiveIndex` (Inspector-set, fixed) — NOT
// `activeIndex` (editor canvas's current view, follows clicks). The two
// attributes are intentionally decoupled so canvas browsing during
// authoring doesn't change which tab the published post opens with.
$active_index  = isset( $attributes['defaultActiveIndex'] ) ? (int) $attributes['defaultActiveIndex'] : 0;
$show_icon     = ! empty( $attributes['showIcon'] );
$show_subtitle = ! empty( $attributes['showSubtitle'] );
$titles        = isset( $attributes['titles'] ) && is_array( $attributes['titles'] )
	? $attributes['titles']
	: array();
$user_class    = isset( $attributes['className'] ) ? trim( (string) $attributes['className'] ) : '';
$anchor        = isset( $attributes['anchor'] ) ? (string) $attributes['anchor'] : '';

// Carousel overflow shows ‹ › paging arrows overlaying the strip's faded
// edges. tabOverflow is per-device (flat-suffix), so render the arrows when
// ANY device resolves to carousel; the `is-fade-*` scroll state (view.js)
// then shows each arrow only while that side still has hidden tabs.
$has_carousel = in_array(
	'carousel',
	array(
		isset( $attributes['tabOverflow'] ) ? (string) $attributes['tabOverflow'] : '',
		isset( $attributes['tabOverflowTablet'] ) ? (string) $attributes['tabOverflowTablet'] : '',
		isset( $attributes['tabOverflowMobile'] ) ? (string) $attributes['tabOverflowMobile'] : '',
	),
	true
);

$inner_blocks  = isset( $block->parsed_block['innerBlocks'] ) && is_array( $block->parsed_block['innerBlocks'] )
	? $block->parsed_block['innerBlocks']
	: array();

if ( '' === $uid || empty( $inner_blocks ) ) {
	return '';
}

if ( isset( $_GET['blocksify_debug'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
	$debug_css = Blocksify_CSS_Generator::generate_block_css( 'blocksify/tabs', $attributes );
	echo "\n<!-- Blocksify Tabs CSS:\n" . esc_html( $debug_css ) . "\n-->\n";
}

$tab_count    = count( $inner_blocks );
$active_index = max( 0, min( $active_index, $tab_count - 1 ) );

$safe_uid = preg_replace( '/[^a-z0-9_-]/i', '', $uid );

// ---------------------------------------------------------------------
// 1. Tablist nav strip — one <button role="tab"> per child Tab.
// ---------------------------------------------------------------------
$tablist_html = '';
foreach ( $inner_blocks as $i => $tab_block ) {
	$title    = isset( $titles[ $i ] ) && is_array( $titles[ $i ] ) ? $titles[ $i ] : array();
	$label    = isset( $title['label'] ) ? (string) $title['label'] : '';
	$subtitle = isset( $title['subtitle'] ) ? (string) $title['subtitle'] : '';
	$icon     = isset( $title['icon'] ) && is_array( $title['icon'] ) ? $title['icon'] : null;

	// No empty-label fallback — an empty title echoes nothing, so icon-only
	// tabs are possible. The editor shows a "Tab title…" RichText placeholder,
	// but that's editor chrome only; it is never persisted or rendered.

	$is_active = $i === $active_index;
	$btn_id    = 'bsy-tab-' . $safe_uid . '-' . $i;
	$panel_id  = 'bsy-tabpanel-' . $safe_uid . '-' . $i;

	$btn_classes = array( 'bsy-tabs__tab' );
	if ( $is_active ) {
		$btn_classes[] = 'is-active';
	}

	$btn_inner = '';
	if ( $show_icon && $icon ) {
		// Icon — resolved by Blocksify_Icons (Lucide sparse bag,
		// custom-svg paste, and inline-svg upload all flow through this).
		// The `svg` field short-circuit honors legacy posts byte-for-byte
		// (pre-PR #173 saves still carry baked SVG markup).
		$icon_svg = Blocksify_Icons::render( $icon );
		$icon_url = ( '' === $icon_svg && isset( $icon['url'] ) && is_string( $icon['url'] ) ) ? $icon['url'] : '';
		if ( '' !== $icon_svg ) {
			$btn_inner .= '<span class="bsy-tabs__tab-icon">' . $icon_svg . '</span>';
		} elseif ( '' !== $icon_url ) {
			$btn_inner .= '<span class="bsy-tabs__tab-icon"><img class="bsy-icon__img" src="'
				. esc_url( $icon_url ) . '" alt="" /></span>';
		}
	}
	// Label + subtitle wrap together in `.bsy-tabs__tab-text` so the pair
	// stacks vertically as one flex item alongside the icon — keeps the
	// subtitle BELOW the label even when icon-position flips the tab's
	// flex-direction to row-reverse / column.
	$text_inner = '';
	if ( '' !== $label ) {
		$text_inner .= '<span class="bsy-tabs__tab-label">' . wp_kses_post( $label ) . '</span>';
	}
	if ( $show_subtitle && '' !== $subtitle ) {
		// Inner span is the grid item that collapses for the "show on active"
		// slide (see `.bsy-tabs__tab-subtitle` in style.css). Editor mirrors
		// this wrapper in edit.js.
		$text_inner .= '<span class="bsy-tabs__tab-subtitle"><span class="bsy-tabs__tab-subtitle-inner">' . wp_kses_post( $subtitle ) . '</span></span>';
	}
	// Skip the text wrapper for an icon-only tab (no label + no subtitle).
	if ( '' !== $text_inner ) {
		$btn_inner .= '<span class="bsy-tabs__tab-text">' . $text_inner . '</span>';
	}

	$btn_attrs  = 'class="' . esc_attr( implode( ' ', $btn_classes ) ) . '"';
	// `<div role="tab">` (not `<button>`) — matches the editor + sidesteps
	// the theme's heavy CTA-button styling; keyboard activation comes from
	// the roving `tabindex` + the Interactivity click / keydown handlers.
	$btn_attrs .= ' role="tab"';
	$btn_attrs .= ' id="' . esc_attr( $btn_id ) . '"';
	$btn_attrs .= ' aria-controls="' . esc_attr( $panel_id ) . '"';
	$btn_attrs .= ' aria-selected="' . ( $is_active ? 'true' : 'false' ) . '"';
	$btn_attrs .= ' tabindex="' . ( $is_active ? '0' : '-1' ) . '"';
	$btn_attrs .= ' data-tab-index="' . (int) $i . '"';
	$btn_attrs .= ' data-wp-on--click="actions.activate"';
	$btn_attrs .= ' data-wp-on--keydown="callbacks.onKeydown"';
	$btn_attrs .= ' data-wp-bind--aria-selected="state.tabs.' . (int) $i . '.ariaSelected"';
	$btn_attrs .= ' data-wp-class--is-active="state.tabs.' . (int) $i . '.isActive"';

	$tablist_html .= '<div ' . $btn_attrs . '>' . $btn_inner . '</div>';
}

// ---------------------------------------------------------------------
// 2. Tabpanels — one <div role="tabpanel"> per child Tab. Render each
//    Tab's INNER blocks (the user content); skip the Tab itself, whose
//    render is filtered out at pre_render_block time.
// ---------------------------------------------------------------------
$panels_html = '';
foreach ( $inner_blocks as $i => $tab_block ) {
	$tab_attrs      = isset( $tab_block['attrs'] ) && is_array( $tab_block['attrs'] ) ? $tab_block['attrs'] : array();
	$tab_uid        = isset( $tab_attrs['uid'] ) ? (string) $tab_attrs['uid'] : '';
	$tab_user_class = isset( $tab_attrs['className'] ) ? trim( (string) $tab_attrs['className'] ) : '';
	$is_active      = $i === $active_index;
	$btn_id         = 'bsy-tab-' . $safe_uid . '-' . $i;
	$panel_id       = 'bsy-tabpanel-' . $safe_uid . '-' . $i;

	$panel_inner   = '';
	$panel_children = isset( $tab_block['innerBlocks'] ) && is_array( $tab_block['innerBlocks'] )
		? $tab_block['innerBlocks']
		: array();
	foreach ( $panel_children as $child_block ) {
		$panel_inner .= render_block( $child_block );
	}

	$panel_classes = array();
	if ( '' !== $tab_uid ) {
		$panel_classes[] = 'bsy-tab-' . preg_replace( '/[^a-z0-9_-]/i', '', $tab_uid );
	}
	// Stable BEM marker so the parent Tabs's `Tab Content` StylingRepeater
	// (and any future per-panel styling) can target every panel with one
	// selector that matches both frontend (this) and editor (set on the
	// Tab block via useBlockProps in src/blocks/tab/edit.js). Role
	// attribute can't be the selector target — WP's BlockListBlock
	// wrapper overrides `role="tabpanel"` to `role="document"` in the
	// editor for keyboard nav.
	$panel_classes[] = 'bsy-tabs__panel';
	if ( '' !== $tab_user_class ) {
		$panel_classes[] = $tab_user_class;
	}

	$panel_attrs  = 'class="' . esc_attr( implode( ' ', $panel_classes ) ) . '"';
	$panel_attrs .= ' role="tabpanel"';
	$panel_attrs .= ' id="' . esc_attr( $panel_id ) . '"';
	$panel_attrs .= ' aria-labelledby="' . esc_attr( $btn_id ) . '"';
	$panel_attrs .= ' data-tab-index="' . (int) $i . '"';
	$panel_attrs .= ' data-wp-bind--hidden="!state.tabs.' . (int) $i . '.isActive"';
	if ( ! $is_active ) {
		$panel_attrs .= ' hidden';
	}

	$panels_html .= '<div ' . $panel_attrs . '>' . $panel_inner . '</div>';
}

// ---------------------------------------------------------------------
// 2b. Carousel arrows — icon-only wrapper-child `<div>`s (not `<button>`, so
//     no theme button styling applies) overlaying the strip's inline edges
//     (CSS positions them flush to the edge — no measured vars). Kept OUTSIDE
//     the tablist so its scroll + fade mask don't clip them. Decorative
//     (`aria-hidden`): every tab is already reachable via the keyboard arrow
//     keys, so these are a pointer convenience. CSS shows each only while that
//     side still has hidden tabs (the `is-fade-*` state).
// ---------------------------------------------------------------------
$arrows_html = '';
if ( $has_carousel ) {
	$chevron_svg = '<svg class="bsy-tabs__arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="%s"/></svg>';
	$arrows_html =
		'<div class="bsy-tabs__arrow bsy-tabs__arrow--prev" aria-hidden="true" data-wp-on--click="actions.carouselPrev">'
			. sprintf( $chevron_svg, 'm15 18-6-6 6-6' ) . '</div>'
		. '<div class="bsy-tabs__arrow bsy-tabs__arrow--next" aria-hidden="true" data-wp-on--click="actions.carouselNext">'
			. sprintf( $chevron_svg, 'm9 18 6-6-6-6' ) . '</div>';
}

// ---------------------------------------------------------------------
// 3. Outer wrapper — carries per-instance class + Interactivity store
//    + initial state context (activeIndex + tabCount). Tablist + panels
//    are direct children for CSS Grid / Flex layout per orientation.
// ---------------------------------------------------------------------
$wrapper_classes = array( 'bsy-tabs-' . $safe_uid );
$wrapper_classes[] = 'is-style-' . preg_replace( '/[^a-z0-9_-]/i', '', $preset );
$wrapper_classes[] = 'is-position-' . $tab_position;
// Subtitle "show on active" — the CSS reveal hangs off this wrapper class.
if ( $show_subtitle && ! empty( $attributes['subtitleOnActive'] ) ) {
	$wrapper_classes[] = 'is-subtitle-active';
}
if ( '' !== $user_class ) {
	$wrapper_classes[] = $user_class;
}

$ctx_json = wp_json_encode(
	array(
		'activeIndex' => $active_index,
		'tabCount'    => $tab_count,
	)
);

$wrapper_attrs  = 'class="' . esc_attr( implode( ' ', $wrapper_classes ) ) . '"';
if ( '' !== $anchor ) {
	$wrapper_attrs .= ' id="' . esc_attr( $anchor ) . '"';
}
$wrapper_attrs .= ' data-wp-interactive="blocksify/tabs"';
$wrapper_attrs .= " data-wp-context='" . esc_attr( $ctx_json ) . "'";

echo '<div ' . $wrapper_attrs . '>'  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	. '<div class="bsy-tabs__tablist" role="tablist" aria-orientation="' . esc_attr( $aria_orientation ) . '"'
		. ' data-wp-init="callbacks.initFade"'
		. ' data-wp-on--scroll="callbacks.updateFade"'
		. ' data-wp-on-window--resize="callbacks.updateFade">'
	. $tablist_html  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	. '</div>'
	. $arrows_html  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	. $panels_html  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	. '</div>';
