<?php
/**
 * Shared standard settings table renderer.
 *
 * @package impleCode\IC
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Renders standard settings tables with optional conditional row groups.
 */
class IC_Settings_Standard_Table {
	/**
	 * Table configuration.
	 *
	 * @var array
	 */
	private $settings;

	/**
	 * Initializes the standard table renderer.
	 *
	 * @param array $args Table configuration.
	 */
	public function __construct( $args = array() ) {
		$defaults = array(
			'settings' => array(),
		);
		$args     = wp_parse_args( is_array( $args ) ? $args : array(), $defaults );
		$settings = is_array( $args['settings'] ) ? $args['settings'] : array();

		$this->settings = wp_parse_args(
			$settings,
			array(
				'rows'                  => array(),
				'groups'                => array(),
				'notices'               => array(),
				'toggle'                => array(),
				'toggles'               => array(),
				'headings'              => array(),
				'table_class'           => '',
				'table_id'              => '',
				'before_table_callback' => null,
				'after_table_callback'  => null,
			)
		);
	}

	/**
	 * Renders the table output.
	 *
	 * @return void
	 */
	public function render() {
		$rows     = $this->normalize_rows( $this->settings['rows'] );
		$groups   = $this->normalize_groups( $this->settings['groups'] );
		$headings = $this->normalize_headings( $this->settings['headings'] );
		$notices  = $this->normalize_notices( $this->settings['notices'] );

		if ( empty( $rows ) && empty( $groups ) && empty( $notices ) ) {
			return;
		}

		$this->render_notices( $notices );

		if ( empty( $rows ) && empty( $groups ) ) {
			return;
		}

		$this->render_table_callback( $this->settings['before_table_callback'] );
		?>
		<table<?php echo $this->table_attributes(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Table attributes are escaped by table_attributes(). ?>>
			<?php if ( ! empty( $headings ) ) : ?>
				<thead>
					<tr>
						<?php foreach ( $headings as $heading ) : ?>
							<th<?php echo $this->heading_attributes( $heading ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Heading attributes are escaped by heading_attributes(). ?>><strong><?php echo esc_html( $heading['label'] ); ?></strong></th>
						<?php endforeach; ?>
					</tr>
				</thead>
			<?php endif; ?>
			<?php if ( ! empty( $rows ) ) : ?>
				<tbody>
					<?php foreach ( $rows as $row ) : ?>
						<?php $this->render_row( $row ); ?>
					<?php endforeach; ?>
				</tbody>
			<?php endif; ?>
			<?php foreach ( $groups as $group ) : ?>
				<tbody class="<?php echo esc_attr( $group['class'] ); ?>">
					<?php foreach ( $group['rows'] as $row ) : ?>
						<?php $this->render_row( $row ); ?>
					<?php endforeach; ?>
				</tbody>
			<?php endforeach; ?>
		</table>
		<?php
		$this->render_table_callback( $this->settings['after_table_callback'] );
		$this->render_toggle_script();
	}

	/**
	 * Renders a table-level callback.
	 *
	 * @param callable|null $callback Callback to render.
	 *
	 * @return void
	 */
	private function render_table_callback( $callback ) {
		if ( ! is_callable( $callback ) ) {
			return;
		}

		call_user_func( $callback, $this->settings, $this );
	}

	/**
	 * Renders configured notices above the table.
	 *
	 * @param array $notices Normalized notices.
	 *
	 * @return void
	 */
	private function render_notices( $notices ) {
		foreach ( $notices as $notice ) {
			$this->render_notice( $notice );
		}
	}

	/**
	 * Renders a single notice.
	 *
	 * @param array $notice Normalized notice configuration.
	 *
	 * @return void
	 */
	private function render_notice( $notice ) {
		switch ( $notice['type'] ) {
			case 'warning':
				if ( function_exists( 'implecode_warning' ) ) {
					echo implecode_warning( $notice['message'], 0 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is generated by implecode_warning().

					return;
				}
				break;
			case 'success':
				if ( function_exists( 'implecode_success' ) ) {
					echo implecode_success( $notice['message'], 0, $notice['paragraph'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is generated by implecode_success().

					return;
				}
				break;
			case 'plus':
				if ( function_exists( 'implecode_plus' ) ) {
					echo implecode_plus( $notice['message'], 0 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is generated by implecode_plus().

					return;
				}
				break;
			case 'info':
			default:
				if ( function_exists( 'implecode_info' ) ) {
					echo implecode_info( $notice['message'], 0, $notice['paragraph'], $notice['dismissible'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is generated by implecode_info().

					return;
				}
				break;
		}

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Notice markup is trusted admin HTML defined in settings configuration.
		echo '<div class="al-box ' . esc_attr( $notice['type'] ) . '">' . $notice['message'] . '</div>';
	}

	/**
	 * Renders one standard settings row.
	 *
	 * @param array $row Row configuration.
	 *
	 * @return void
	 */
	private function render_row( $row ) {
		if ( isset( $row['type'] ) && 'cells' === $row['type'] ) {
			$this->render_cells_row( $row );

			return;
		}

		if ( isset( $row['type'] ) && 'html' === $row['type'] ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Custom HTML rows are trusted admin markup provided by framework/plugin settings definitions.
			echo $row['html'];

			return;
		}

		if ( isset( $row['callback'] ) && is_callable( $row['callback'] ) ) {
			call_user_func( $row['callback'], $row );

			return;
		}

		$type  = $row['type'];
		$label = $row['label'];
		$name  = $row['name'];
		$value = $row['value'];

		switch ( $type ) {
			case 'radio':
				implecode_settings_radio(
					$label,
					$name,
					$value,
					$row['options'],
					1,
					$row['tip'],
					$row['line'],
					$row['class'],
					$row['table_row']
				);
				break;
			case 'checkbox':
				implecode_settings_checkbox(
					$label,
					$name,
					$value,
					1,
					$row['tip'],
					$row['checked_value'],
					$row['class']
				);
				break;
			case 'textarea':
				implecode_settings_textarea(
					$label,
					$name,
					$value,
					1,
					$row['attributes'],
					$row['tip']
				);
				break;
			case 'number':
				implecode_settings_number(
					$label,
					$name,
					$value,
					$row['unit'],
					1,
					$row['step'],
					$row['tip'],
					$row['min'],
					$row['max'],
					$row['class'],
					$row['tr_class'],
					$row['attributes']
				);
				break;
			case 'upload_image':
				$this->render_upload_image_row( $row );
				break;
			case 'dropdown':
				implecode_settings_dropdown(
					$label,
					$name,
					$value,
					$row['options'],
					1,
					$row['attributes'],
					$row['tip']
				);
				break;
			case 'text':
			default:
				implecode_settings_text(
					$label,
					$name,
					$value,
					$row['required'],
					1,
					$row['class'],
					$row['tip'],
					$row['disabled'],
					$row['attributes']
				);
				break;
		}
	}

	/**
	 * Renders an upload-image settings row.
	 *
	 * @param array $row Row configuration.
	 *
	 * @return void
	 */
	private function render_upload_image_row( $row ) {
		$button_label = '' !== $row['button_label'] ? $row['button_label'] : $row['label'];
		?>
		<tr>
			<td><?php echo esc_html( $row['label'] ); ?>:</td>
			<td>
				<?php
				if ( function_exists( 'implecode_upload_image' ) ) {
					implecode_upload_image(
						$button_label,
						$row['name'],
						$row['value'],
						$row['default_image'],
						$row['upload_image_id'],
						1,
						$row['id']
					);
				}
				?>
			</td>
		</tr>
		<?php
	}

	/**
	 * Renders a custom multi-cell row.
	 *
	 * @param array $row Row configuration.
	 *
	 * @return void
	 */
	private function render_cells_row( $row ) {
		?>
		<tr<?php echo $this->row_attributes( $row ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Row attributes are escaped by row_attributes(). ?>>
			<?php foreach ( $row['cells'] as $cell ) : ?>
				<<?php echo tag_escape( $cell['tag'] ); ?><?php echo $this->cell_attributes( $cell ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Cell attributes are escaped by cell_attributes(). ?>>
					<?php
					// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Custom cell content is trusted admin markup provided by framework/plugin settings definitions.
					echo $cell['content'];
					?>
				</<?php echo tag_escape( $cell['tag'] ); ?>>
			<?php endforeach; ?>
		</tr>
		<?php
	}

	/**
	 * Renders the conditional group toggle script.
	 *
	 * @return void
	 */
	private function render_toggle_script() {
		$toggles = $this->normalize_toggles();
		if ( empty( $toggles ) ) {
			return;
		}
		?>
		<script>
			jQuery( function ( $ ) {
				var toggles = <?php echo wp_json_encode( $toggles ); ?>;

				function selectedValue( toggle ) {
					var selectorFields = $( toggle.selector );

					if ( ! selectorFields.length ) {
						return '';
					}

					if ( 'checked' === toggle.type ) {
						return selectorFields.first().is( ':checked' ) ? 'checked' : 'unchecked';
					}

					if ( selectorFields.first().is( ':radio' ) ) {
						return selectorFields.filter( ':checked' ).val() || '';
					}

					return selectorFields.val() || '';
				}

				function toggleRows() {
					var targetStates = {};

					$.each(
						toggles,
						function ( index, toggle ) {
							var currentValue = selectedValue( toggle );

							$.each(
								toggle.groups,
								function ( groupIndex, group ) {
									if ( typeof targetStates[ group.target ] === 'undefined' ) {
										targetStates[ group.target ] = true;
									}

									targetStates[ group.target ] = targetStates[ group.target ] && currentValue === group.value;
								}
							);
						}
					);

					$.each(
						targetStates,
						function ( target, isVisible ) {
							$( target ).toggle( isVisible );
						}
					);
				}

				$.each(
					toggles,
					function ( index, toggle ) {
						var selectorFields = $( toggle.selector );

						selectorFields.on( 'change', toggleRows );
					}
				);

				toggleRows();
			} );
		</script>
		<?php
	}

	/**
	 * Normalizes all toggle settings.
	 *
	 * @return array
	 */
	private function normalize_toggles() {
		$toggles = array();

		if ( ! empty( $this->settings['toggle'] ) ) {
			$toggle = $this->normalize_toggle( $this->settings['toggle'] );
			if ( ! empty( $toggle ) ) {
				$toggles[] = $toggle;
			}
		}

		if ( ! is_array( $this->settings['toggles'] ) ) {
			return $toggles;
		}

		foreach ( $this->settings['toggles'] as $toggle ) {
			$toggle = $this->normalize_toggle( $toggle );
			if ( ! empty( $toggle ) ) {
				$toggles[] = $toggle;
			}
		}

		return $toggles;
	}

	/**
	 * Normalizes row definitions.
	 *
	 * @param array $rows Raw row config.
	 *
	 * @return array
	 */
	private function normalize_rows( $rows ) {
		$normalized = array();

		if ( ! is_array( $rows ) ) {
			return $normalized;
		}

		foreach ( $rows as $row ) {
			$normalized_row = $this->normalize_row( $row );
			if ( ! empty( $normalized_row ) ) {
				$normalized[] = $normalized_row;
			}
		}

		return $normalized;
	}

	/**
	 * Normalizes grouped row definitions.
	 *
	 * @param array $groups Raw grouped row config.
	 *
	 * @return array
	 */
	private function normalize_groups( $groups ) {
		$normalized = array();

		if ( ! is_array( $groups ) ) {
			return $normalized;
		}

		foreach ( $groups as $group ) {
			if ( ! is_array( $group ) || empty( $group['class'] ) ) {
				continue;
			}

			$rows = isset( $group['rows'] ) ? $this->normalize_rows( $group['rows'] ) : array();
			if ( empty( $rows ) ) {
				continue;
			}

			$normalized[] = array(
				'class' => (string) $group['class'],
				'rows'  => $rows,
			);
		}

		return $normalized;
	}

	/**
	 * Normalizes heading definitions.
	 *
	 * @param array $headings Raw heading config.
	 *
	 * @return array
	 */
	private function normalize_headings( $headings ) {
		$normalized = array();

		if ( ! is_array( $headings ) ) {
			return $normalized;
		}

		foreach ( $headings as $heading ) {
			if ( is_string( $heading ) && '' !== $heading ) {
				$normalized[] = array(
					'label' => $heading,
					'width' => '',
					'class' => '',
				);
				continue;
			}

			if ( ! is_array( $heading ) || empty( $heading['label'] ) ) {
				continue;
			}

			$normalized[] = wp_parse_args(
				$heading,
				array(
					'label' => '',
					'width' => '',
					'class' => '',
				)
			);
		}

		return $normalized;
	}

	/**
	 * Normalizes table notices.
	 *
	 * @param array $notices Raw notice config.
	 *
	 * @return array
	 */
	private function normalize_notices( $notices ) {
		$normalized = array();

		if ( ! is_array( $notices ) ) {
			return $normalized;
		}

		foreach ( $notices as $notice ) {
			if ( is_string( $notice ) && '' !== $notice ) {
				$normalized[] = array(
					'type'        => 'info',
					'message'     => $notice,
					'paragraph'   => 0,
					'dismissible' => false,
				);
				continue;
			}

			if ( ! is_array( $notice ) || empty( $notice['message'] ) ) {
				continue;
			}

			$type = isset( $notice['type'] ) ? (string) $notice['type'] : 'info';
			if ( ! in_array( $type, array( 'info', 'warning', 'success', 'plus' ), true ) ) {
				$type = 'info';
			}

			$normalized[] = wp_parse_args(
				$notice,
				array(
					'type'        => $type,
					'message'     => '',
					'paragraph'   => 0,
					'dismissible' => false,
				)
			);
		}

		return $normalized;
	}

	/**
	 * Normalizes a single row definition.
	 *
	 * @param array $row Raw row config.
	 *
	 * @return array
	 */
	private function normalize_row( $row ) {
		if ( ! is_array( $row ) ) {
			return array();
		}

		if ( isset( $row['type'] ) && 'cells' === $row['type'] ) {
			$row['cells'] = $this->normalize_cells( isset( $row['cells'] ) ? $row['cells'] : array() );

			if ( empty( $row['cells'] ) ) {
				return array();
			}

			return wp_parse_args(
				$row,
				array(
					'type'       => 'cells',
					'class'      => '',
					'attributes' => array(),
					'cells'      => array(),
				)
			);
		}

		if ( isset( $row['type'] ) && 'html' === $row['type'] ) {
			return wp_parse_args(
				$row,
				array(
					'type' => 'html',
					'html' => '',
				)
			);
		}

		if ( isset( $row['callback'] ) && is_callable( $row['callback'] ) ) {
			return $row;
		}

		if ( empty( $row['name'] ) ) {
			return array();
		}

		return wp_parse_args(
			$row,
			array(
				'type'            => 'text',
				'label'           => '',
				'name'            => '',
				'value'           => '',
				'options'         => array(),
				'tip'             => '',
				'attributes'      => null,
				'required'        => null,
				'class'           => null,
				'disabled'        => '',
				'line'            => '<br>',
				'table_row'       => true,
				'checked_value'   => 1,
				'unit'            => '',
				'step'            => 1,
				'min'             => null,
				'max'             => null,
				'tr_class'        => null,
				'button_label'    => '',
				'default_image'   => null,
				'upload_image_id' => 'url',
				'id'              => '',
			)
		);
	}

	/**
	 * Normalizes toggle settings.
	 *
	 * @param array $toggle Raw toggle config.
	 *
	 * @return array
	 */
	private function normalize_toggle( $toggle ) {
		$toggle = wp_parse_args(
			is_array( $toggle ) ? $toggle : array(),
			array(
				'selector' => '',
				'groups'   => array(),
				'type'     => 'value',
			)
		);
		$groups = array();

		foreach ( $toggle['groups'] as $group ) {
			if ( ! is_array( $group ) || empty( $group['target'] ) || ! array_key_exists( 'value', $group ) ) {
				continue;
			}

			$groups[] = array(
				'target' => (string) $group['target'],
				'value'  => (string) $group['value'],
			);
		}

		$toggle['groups'] = $groups;
		$toggle['type']   = 'checked' === $toggle['type'] ? 'checked' : 'value';

		if ( empty( $toggle['selector'] ) || empty( $toggle['groups'] ) ) {
			return array();
		}

		return $toggle;
	}

	/**
	 * Normalizes cell definitions for custom rows.
	 *
	 * @param array $cells Raw cell config.
	 *
	 * @return array
	 */
	private function normalize_cells( $cells ) {
		$normalized = array();

		if ( ! is_array( $cells ) ) {
			return $normalized;
		}

		foreach ( $cells as $cell ) {
			if ( ! is_array( $cell ) ) {
				continue;
			}

			$tag = isset( $cell['tag'] ) && 'th' === $cell['tag'] ? 'th' : 'td';

			$normalized[] = wp_parse_args(
				$cell,
				array(
					'tag'        => $tag,
					'content'    => '',
					'class'      => '',
					'colspan'    => '',
					'rowspan'    => '',
					'width'      => '',
					'attributes' => array(),
				)
			);
		}

		return $normalized;
	}

	/**
	 * Returns table HTML attributes.
	 *
	 * @return string
	 */
	private function table_attributes() {
		$attributes = '';

		if ( '' !== $this->settings['table_class'] ) {
			$attributes .= ' class="' . esc_attr( $this->settings['table_class'] ) . '"';
		}

		if ( '' !== $this->settings['table_id'] ) {
			$attributes .= ' id="' . esc_attr( $this->settings['table_id'] ) . '"';
		}

		return $attributes;
	}

	/**
	 * Returns table heading HTML attributes.
	 *
	 * @param array $heading Normalized heading.
	 *
	 * @return string
	 */
	private function heading_attributes( $heading ) {
		$attributes = '';

		if ( '' !== $heading['width'] ) {
			$attributes .= ' style="width:' . esc_attr( $heading['width'] ) . '"';
		}

		if ( '' !== $heading['class'] ) {
			$attributes .= ' class="' . esc_attr( $heading['class'] ) . '"';
		}

		return $attributes;
	}

	/**
	 * Returns row HTML attributes.
	 *
	 * @param array $row Normalized row.
	 *
	 * @return string
	 */
	private function row_attributes( $row ) {
		$attributes = '';

		if ( '' !== $row['class'] ) {
			$attributes .= ' class="' . esc_attr( $row['class'] ) . '"';
		}

		$attributes .= $this->custom_attributes( $row['attributes'] );

		return $attributes;
	}

	/**
	 * Returns cell HTML attributes.
	 *
	 * @param array $cell Normalized cell.
	 *
	 * @return string
	 */
	private function cell_attributes( $cell ) {
		$attributes = '';

		if ( '' !== $cell['class'] ) {
			$attributes .= ' class="' . esc_attr( $cell['class'] ) . '"';
		}

		if ( '' !== $cell['colspan'] ) {
			$attributes .= ' colspan="' . absint( $cell['colspan'] ) . '"';
		}

		if ( '' !== $cell['rowspan'] ) {
			$attributes .= ' rowspan="' . absint( $cell['rowspan'] ) . '"';
		}

		if ( '' !== $cell['width'] ) {
			$attributes .= ' width="' . esc_attr( $cell['width'] ) . '"';
		}

		$attributes .= $this->custom_attributes( $cell['attributes'] );

		return $attributes;
	}

	/**
	 * Returns custom HTML attributes from a key-value list.
	 *
	 * @param array $attributes Raw attributes.
	 *
	 * @return string
	 */
	private function custom_attributes( $attributes ) {
		$html = '';

		if ( ! is_array( $attributes ) ) {
			return $html;
		}

		foreach ( $attributes as $name => $value ) {
			if ( ! is_scalar( $name ) || '' === (string) $name ) {
				continue;
			}

			if ( ! is_scalar( $value ) ) {
				continue;
			}

			$html .= ' ' . sanitize_key( (string) $name ) . '="' . esc_attr( (string) $value ) . '"';
		}

		return $html;
	}
}
