<?php
defined( 'ABSPATH' ) || exit;

/**
 * Shared Split Text animation config builder.
 *
 * Used by Jalpi_Element_Split_Text (the standalone widget) and by the opt-in
 * "Split Animation" group retrofitted onto existing text widgets. Keeping the
 * whitelists in one place means the widget and the retrofit can never drift
 * apart, and every value that reaches the page has passed the same validation.
 *
 * Backward compatibility: split_text_attr() returns an EMPTY STRING unless
 * $settings['st_enabled'] is truthy. Existing saved content has no such key, so
 * every existing widget renders byte-identically to before this file existed.
 */
trait Jalpi_Split_Text {

	/**
	 * Animation names accepted from saved content. Anything not on this list is
	 * rejected outright rather than coerced — these values become CSS custom
	 * property values on the page, so free text must never reach them.
	 */
	private static function st_animations(): array {
		return [
			'fadeUp', 'fadeDown', 'fadeLeft', 'fadeRight',
			'zoomIn', 'zoomOut', 'rotate', 'blurReveal',
			'scale', 'slide', 'flip', 'elastic', 'bounce',
			'typewriter', 'wave', 'maskReveal', 'clipReveal',
			'glowReveal', 'letterSpacing', 'gradientReveal',
		];
	}

	private static function st_split_types(): array {
		return [ 'chars', 'words', 'lines', 'paragraphs' ];
	}

	private static function st_triggers(): array {
		return [ 'load', 'scroll' ];
	}

	/**
	 * Easing keywords plus a strictly-shaped cubic-bezier(). Rejecting anything
	 * else keeps arbitrary text out of the animation-timing-function value.
	 */
	private static function st_sanitize_ease( string $ease ): string {
		$keywords = [ 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out' ];
		if ( in_array( $ease, $keywords, true ) ) {
			return $ease;
		}
		if ( preg_match( '/^cubic-bezier\(\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*\)$/', $ease ) ) {
			return $ease;
		}
		return '';
	}

	/**
	 * A linear-gradient(...) built only from validated colours and an integer
	 * angle. Assembled here rather than passed through, so no caller-supplied
	 * string ever lands in the stylesheet.
	 */
	private static function st_sanitize_gradient( array $g ): string {
		$from = Jalpi_Sanitizer::sanitize_color( $g['from'] ?? '' );
		$to   = Jalpi_Sanitizer::sanitize_color( $g['to'] ?? '' );
		if ( ! $from || ! $to ) {
			return '';
		}
		$angle = isset( $g['angle'] ) ? (int) $g['angle'] : 90;
		$angle = max( 0, min( 360, $angle ) );
		return "linear-gradient({$angle}deg,{$from},{$to})";
	}

	/**
	 * Build the data-jalpi-st attribute for a node.
	 *
	 * @param array $settings Node settings.
	 * @return string  ' data-jalpi-st="{...}"', or '' when the feature is off.
	 */
	protected function build_split_text_attr( array $settings ): string {
		// The backward-compatibility gate. No key -> no attribute -> no change.
		if ( empty( $settings['st_enabled'] ) ) {
			return '';
		}

		$animation = (string) ( $settings['st_animation'] ?? '' );
		if ( ! in_array( $animation, self::st_animations(), true ) ) {
			return '';
		}

		$split = (string) ( $settings['st_split'] ?? 'chars' );
		if ( ! in_array( $split, self::st_split_types(), true ) ) {
			$split = 'chars';
		}

		$trigger = (string) ( $settings['st_trigger'] ?? 'scroll' );
		if ( ! in_array( $trigger, self::st_triggers(), true ) ) {
			$trigger = 'scroll';
		}

		$cfg = [
			'animation' => $animation,
			'splitBy'   => $split,
			'trigger'   => $trigger,
		];

		// Numerics are clamped to sane ranges; a huge stagger on a char split
		// would otherwise leave text invisible for minutes.
		$cfg['duration'] = max( 0, min( 20000, (int) ( $settings['st_duration'] ?? 800 ) ) );
		$cfg['delay']    = max( 0, min( 20000, (int) ( $settings['st_delay'] ?? 0 ) ) );
		$cfg['stagger']  = max( 0, min( 2000, (int) ( $settings['st_stagger'] ?? 40 ) ) );

		$repeat = (int) ( $settings['st_repeat'] ?? 1 );
		// -1 is the sentinel for "infinite"; everything else is a real count.
		$cfg['repeat'] = ( -1 === $repeat ) ? -1 : max( 1, min( 100, $repeat ) );

		$ease = self::st_sanitize_ease( (string) ( $settings['st_ease'] ?? '' ) );
		if ( $ease ) {
			$cfg['ease'] = $ease;
		}

		if ( ! empty( $settings['st_replay'] ) ) {
			$cfg['replay'] = true;
		}
		if ( ! empty( $settings['st_reverse'] ) ) {
			$cfg['reverse'] = true;
		}

		$pct = (int) ( $settings['st_viewport_pct'] ?? 20 );
		$cfg['viewportPct'] = max( 0, min( 100, $pct ) );

		// Trigger offset: digits plus a unit only, assembled from parts.
		if ( ! empty( $settings['st_offset'] ) ) {
			$offset = (string) $settings['st_offset'];
			if ( preg_match( '/^(\d{1,4})(px|%|vh)$/', $offset, $m ) ) {
				$cfg['offset'] = $m[1] . $m[2];
			}
		}

		if ( ! empty( $settings['st_gradient'] ) && is_array( $settings['st_gradient'] ) ) {
			$grad = self::st_sanitize_gradient( $settings['st_gradient'] );
			if ( $grad ) {
				$cfg['gradient'] = $grad;
			}
		}

		$json = wp_json_encode( $cfg );
		if ( ! $json ) {
			return '';
		}

		return ' data-jalpi-st="' . esc_attr( $json ) . '"';
	}

	/**
	 * True when this node should pull in the shared frontend script.
	 * Mirrors the split_text_attr() gate so JS is never enqueued for content
	 * that does not animate.
	 */
	protected function split_text_enabled( array $settings ): bool {
		return ! empty( $settings['st_enabled'] )
			&& in_array( (string) ( $settings['st_animation'] ?? '' ), self::st_animations(), true );
	}
}
