<?php
/**
 * PK Core - renders one Site Profile question, and reads it back.
 *
 * The wizard and the full page are two VIEWS of one question set
 * (PKCORE_Questions), so they must also share one renderer and one collector.
 * Two renderers would drift, and a drifted collector is the worst kind: the
 * screen looks right and the value written is wrong.
 *
 * collect() is deliberately the mirror image of render(). Whatever render()
 * emits for a type, collect() reads for that type, and neither knows anything
 * about a specific question.
 *
 * @package PublisherKits\Core
 * @since   0.10.0
 */

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

if ( ! class_exists( 'PKCORE_Fields' ) ) {

	/**
	 * Class PKCORE_Fields
	 *
	 * @since 0.10.0
	 */
	class PKCORE_Fields {

		/**
		 * Render one question.
		 *
		 * @param string $key   Profile key.
		 * @param array  $spec  Question spec from PKCORE_Questions.
		 * @param mixed  $value Current value.
		 * @param string $tier  'wizard' or 'full' - controls which sub-fields of a
		 *                      nested question appear.
		 * @return void
		 */
		public static function render( $key, $spec, $value, $tier = 'full' ) {
			$type = isset( $spec['type'] ) ? $spec['type'] : 'text';

			echo '<div class="pkcore-q pkcore-q--' . esc_attr( $type ) . '">';
			echo '<label class="pkcore-q__label" for="' . esc_attr( 'pkcore_' . $key ) . '">' . esc_html( $spec['label'] ) . '</label>';

			if ( ! empty( $spec['help'] ) ) {
				echo '<p class="pkcore-q__help">' . esc_html( $spec['help'] ) . '</p>';
			}

			self::render_input( $key, $spec, $value, $tier );

			// What this answer actually feeds. Generated, never hand-written, so a
			// question that feeds nothing says nothing - which is how a dead
			// question announces itself instead of quietly persisting.
			self::render_feeds( $key );

			echo '</div>';
		}

		/**
		 * The input itself.
		 *
		 * @param string $key   Profile key.
		 * @param array  $spec  Question spec.
		 * @param mixed  $value Current value.
		 * @param string $tier  Tier being rendered.
		 * @return void
		 */
		private static function render_input( $key, $spec, $value, $tier ) {
			$id   = 'pkcore_' . $key;
			$type = isset( $spec['type'] ) ? $spec['type'] : 'text';

			switch ( $type ) {

				case 'textarea':
					echo '<textarea class="large-text" rows="4" id="' . esc_attr( $id ) . '" name="' . esc_attr( $key ) . '">' . esc_textarea( (string) $value ) . '</textarea>';
					break;

				case 'number':
					echo '<input type="number" min="0" step="1" class="small-text" id="' . esc_attr( $id ) . '" name="' . esc_attr( $key ) . '" value="' . esc_attr( (string) (int) $value ) . '">';
					break;

				case 'select':
					$options = self::options_for( $spec );
					echo '<select id="' . esc_attr( $id ) . '" name="' . esc_attr( $key ) . '">';
					echo '<option value="">' . esc_html__( 'Not set', 'publisherkits-socialpromoter' ) . '</option>';
					foreach ( $options as $slug => $label ) {
						echo '<option value="' . esc_attr( $slug ) . '"' . selected( (string) $value, (string) $slug, false ) . '>' . esc_html( $label ) . '</option>';
					}
					echo '</select>';
					break;

				case 'checkboxes':
					$options  = self::options_for( $spec );
					$selected = is_array( $value ) ? array_map( 'strval', $value ) : array();
					if ( empty( $options ) ) {
						// A closed vocabulary with no values is not an empty answer,
						// it is an unanswerable question. Say so rather than render
						// an empty box the operator cannot use.
						echo '<p class="pkcore-q__unavailable">' . esc_html__( 'No options are available for this question yet.', 'publisherkits-socialpromoter' ) . '</p>';
						break;
					}
					echo '<div class="pkcore-q__grid">';
					foreach ( $options as $slug => $label ) {
						echo '<label class="pkcore-q__check"><input type="checkbox" name="' . esc_attr( $key ) . '[]" value="' . esc_attr( $slug ) . '"' . checked( in_array( (string) $slug, $selected, true ), true, false ) . '> ' . esc_html( $label ) . '</label>';
					}
					// An all-unchecked group posts nothing, which is
					// indistinguishable from "the field was not on this form".
					// The marker makes "cleared" legible to collect().
					echo '<input type="hidden" name="' . esc_attr( $key ) . '__present" value="1">';
					echo '</div>';
					break;

				case 'list':
					$lines = is_array( $value ) ? implode( "\n", array_map( 'strval', $value ) ) : (string) $value;
					echo '<textarea class="large-text code" rows="5" id="' . esc_attr( $id ) . '" name="' . esc_attr( $key ) . '" placeholder="' . esc_attr__( 'One per line', 'publisherkits-socialpromoter' ) . '">' . esc_textarea( $lines ) . '</textarea>';
					break;

				case 'nested':
					$value  = is_array( $value ) ? $value : array();
					$fields = isset( $spec['fields'] ) && is_array( $spec['fields'] ) ? $spec['fields'] : array();
					echo '<div class="pkcore-q__nested">';
					foreach ( $fields as $sub => $sub_spec ) {
						// On the wizard, show only the sub-fields tiered for it.
						if ( 'wizard' === $tier && 'wizard' !== ( isset( $sub_spec['tier'] ) ? $sub_spec['tier'] : 'full' ) ) {
							continue;
						}
						$sub_spec['label'] = isset( $sub_spec['label'] ) ? $sub_spec['label'] : $sub;
						self::render( $key . '[' . $sub . ']', $sub_spec, isset( $value[ $sub ] ) ? $value[ $sub ] : '', $tier );
					}
					echo '</div>';
					break;

				case 'text':
				default:
					echo '<input type="text" class="regular-text" id="' . esc_attr( $id ) . '" name="' . esc_attr( $key ) . '" value="' . esc_attr( (string) $value ) . '">';
					break;
			}
		}

		/**
		 * The "this feeds" line.
		 *
		 * @param string $key Profile key.
		 * @return void
		 */
		private static function render_feeds( $key ) {
			// Nested sub-fields carry a bracketed key; the consumer matrix is keyed
			// on the parent, so only the parent renders the line.
			if ( false !== strpos( $key, '[' ) ) {
				return;
			}
			if ( ! class_exists( 'PKCORE_Consumers' ) ) {
				return;
			}

			$feeds = PKCORE_Consumers::feeds( $key );
			if ( ! empty( $feeds ) ) {
				echo '<p class="pkcore-q__feeds"><span class="pkcore-q__feeds-label">' . esc_html__( 'Used by:', 'publisherkits-socialpromoter' ) . '</span> ' . esc_html( implode( ', ', $feeds ) ) . '</p>';
				return;
			}

			// Installed, intends to read it, does not yet. A distinct state from
			// both "used" and "unused": rendering against a real site showed 12 of
			// 22 questions reading "not used by any plugin", including the two the
			// gap-reporting case depends on. True, but it tells the operator half
			// the form is pointless - and they would be right to stop filling it
			// in. "Will be used" is a weaker claim, and an honest one.
			$planned = PKCORE_Consumers::planned_for( $key );
			if ( ! empty( $planned ) ) {
				echo '<p class="pkcore-q__planned"><span class="pkcore-q__feeds-label">' . esc_html__( 'Will be used by:', 'publisherkits-socialpromoter' ) . '</span> ' . esc_html( implode( ', ', $planned ) ) . ' <em>' . esc_html__( '(not yet - this answer is stored, and starts working when that plugin updates)', 'publisherkits-socialpromoter' ) . '</em></p>';
				return;
			}

			// Not installed, but would use it. Honest promotion: the same matrix,
			// so it can only describe a capability the plugin genuinely has.
			$could = PKCORE_Consumers::could_feed( $key );
			if ( ! empty( $could ) ) {
				$first = reset( $could );
				echo '<p class="pkcore-q__could"><span class="pkcore-q__feeds-label">' . esc_html__( 'Would also be used by:', 'publisherkits-socialpromoter' ) . '</span> ';
				echo '<a href="' . esc_url( $first['url'] ) . '" target="_blank" rel="noopener">' . esc_html( $first['label'] ) . '</a> &mdash; ' . esc_html( $first['blurb'] );
				echo '</p>';
				return;
			}

			// Nothing reads it, installed or not. Say so plainly rather than leave
			// a blank the operator reads as "presumably useful".
			echo '<p class="pkcore-q__unused">' . esc_html__( 'Not currently used by any PublisherKits plugin.', 'publisherkits-socialpromoter' ) . '</p>';
		}

		/**
		 * Options for a select/checkbox question.
		 *
		 * @param array $spec Question spec.
		 * @return array<string,string>
		 */
		private static function options_for( $spec ) {
			if ( ! empty( $spec['options'] ) && is_array( $spec['options'] ) ) {
				return $spec['options']; // Contributed questions carry their own.
			}
			if ( ! empty( $spec['vocab'] ) && class_exists( 'PKCORE_Vocab' ) ) {
				return PKCORE_Vocab::options( $spec['vocab'] );
			}
			return array();
		}

		/**
		 * Read one question back from a submitted form.
		 *
		 * Returns null when the field was NOT PRESENT on the submitted form, which
		 * is different from present-and-empty. The caller must not write a null:
		 * that distinction is what stops a short wizard from blanking answers the
		 * full page collected.
		 *
		 * @param string $key  Profile key.
		 * @param array  $spec Question spec.
		 * @param array  $post Raw $_POST.
		 * @param string $tier Tier that was rendered.
		 * @return mixed|null
		 */
		public static function collect( $key, $spec, $post, $tier = 'full' ) {
			$type = isset( $spec['type'] ) ? $spec['type'] : 'text';

			if ( 'nested' === $type ) {
				$fields = isset( $spec['fields'] ) && is_array( $spec['fields'] ) ? $spec['fields'] : array();
				$raw    = isset( $post[ $key ] ) && is_array( $post[ $key ] ) ? $post[ $key ] : array();
				$out    = array();
				$any    = false;
				foreach ( $fields as $sub => $sub_spec ) {
					if ( 'wizard' === $tier && 'wizard' !== ( isset( $sub_spec['tier'] ) ? $sub_spec['tier'] : 'full' ) ) {
						continue; // Not rendered, so not collected - and not blanked.
					}
					$value = self::collect( $sub, $sub_spec, $raw, $tier );
					if ( null !== $value ) {
						$out[ $sub ] = $value;
						$any         = true;
					}
				}
				return $any ? $out : null;
			}

			if ( 'checkboxes' === $type ) {
				// Present-and-cleared is signalled by the marker, not by the
				// checkbox array, which vanishes entirely when nothing is ticked.
				if ( empty( $post[ $key . '__present' ] ) ) {
					return null;
				}
				return isset( $post[ $key ] ) && is_array( $post[ $key ] ) ? array_map( 'strval', $post[ $key ] ) : array();
			}

			if ( ! array_key_exists( $key, $post ) ) {
				return null;
			}

			switch ( $type ) {
				case 'number':
					return max( 0, (int) $post[ $key ] );
				case 'list':
					$raw   = (string) $post[ $key ];
					$lines = preg_split( '/[\r\n]+/', $raw );
					$out   = array();
					foreach ( (array) $lines as $line ) {
						$line = trim( (string) $line );
						if ( '' !== $line ) {
							$out[] = $line;
						}
					}
					return $out;
				default:
					return (string) $post[ $key ];
			}
		}
	}
}
