<?php

/**
 * The base class for all SiteOrigin_Widget form fields.
 *
 * Class SiteOrigin_Widget_Field
 */
abstract class SiteOrigin_Widget_Field_Base {
	/* ============================================================================================================== */
	/* CORE FIELD PROPERTIES                                                                                          */
	/* Properties which are essential to successful rendering of fields and saving of data input into fields.         */
	/* ============================================================================================================== */

	/**
	 * The base name for this field. It is used in the generation of HTML element id and name attributes.
	 *
	 * @var string
	 */
	protected $base_name;
	/**
	 * The rendered HTML element id attribute.
	 *
	 * @var string
	 */
	protected $element_id;
	/**
	 * The rendered HTML element name attribute
	 *
	 * @var string
	 */
	protected $element_name;
	/**
	 * The field configuration options.
	 *
	 * @var array
	 */
	protected $field_options;
	/**
	 * Variables may be added to this array which will be propagated to the front end for use in dynamic rendering.
	 *
	 * @var array
	 */
	protected $javascript_variables;

	/* ============================================================================================================== */
	/* BASE FIELD CONFIGURATION PROPERTIES                                                                            */
	/* Common configuration properties used by all fields.                                                            */
	/* ============================================================================================================== */

	/**
	 * The type of the field.
	 *
	 * @var string
	 */
	protected $type;
	/**
	 * Render a label for the field with the given value.
	 *
	 * @var string
	 */
	protected $label;
	/**
	 * The field will be prepopulated with this default value.
	 *
	 * @var mixed
	 */
	protected $default;
	/**
	 * Render small italic text below the field to describe the field's purpose.
	 *
	 * @var string
	 */
	protected $description;
	/**
	 * Append '(Optional)' to this field's label as a small green superscript.
	 *
	 * @var bool
	 */
	protected $optional;
	/**
	 * @var bool|string Is this field required.
	 */
	protected $required;
	/**
	 * Specifies an additional sanitization to be performed. Available sanitizations are `text`, `email` and `url`. If the
	 * specified sanitization isn't recognized it is assumed to be a custom sanitization and a filter is applied using
	 * the pattern `'siteorigin_widgets_sanitize_field_' . $sanitize`, in case the sanitization is defined elsewhere.
	 *
	 * @var string
	 */
	protected $sanitize;
	/**
	 * Reference to the parent widget required for creating child fields.
	 *
	 * @var SiteOrigin_Widget
	 */
	protected $for_widget;
	/**
	 * An array of field names of parent containers.
	 *
	 * @var array
	 */
	protected $parent_container;
	/**
	 * Whether or not this field contains other fields.
	 *
	 * @var bool
	 */
	protected $is_container;
	/**
	 * Additional CSS classes to output in this field's HTML class attribute. It is left up to the field's render_field
	 * function to output these classes.
	 *
	 * @var array
	 */
	protected $input_css_classes;

	/* ============================================================================================================== */
	/* FIELD STATES PROPERTIES                                                                                        */
	/* Configuration of field state emitters and handlers.                                                            */
	/* See https://siteorigin.com/docs/widgets-bundle/form-building/state-emitters/ for more detail on the topic of   */
	/* state emitters and handlers.                                                                                   */
	/* ============================================================================================================== */

	/**
	 * Specifies the callback type and arguments to use when deciding on the state to be emitted.
	 *
	 * @var array
	 */
	protected $state_emitter;
	/**
	 * Specifies the different possible states to be handled by this field and the resulting effect of the each state.
	 *
	 * @var array
	 */
	protected $state_handler;

	protected $state_handler_initial;

	/**
	 * @param $base_name string The name of the field.
	 * @param $element_id string The id to be used as the id attribute of the wrapping HTML element.
	 * @param $element_name string The name to be used as the name attribute of the wrapping HTML element.
	 * @param $field_options array Configuration for the field.
	 * @param SiteOrigin_Widget $for_widget
	 * @param array $parent_container
	 *
	 * @throws InvalidArgumentException
	 */
	public function __construct( $base_name, $element_id, $element_name, $field_options, $for_widget = null, $parent_container = array() ) {
		if ( isset( $field_options['type'] ) ) {
			$this->type = $field_options['type'];
		} else {
			throw new InvalidArgumentException( 'SiteOrigin_Widget_Field_Base::__construct: $field_options must contain a \'type\' field.' );
		}

		$this->base_name = $base_name;
		$this->element_id = $element_id;
		$this->element_name = $element_name;
		$this->field_options = $field_options;
		$this->javascript_variables = array();

		$this->for_widget = $for_widget;
		$this->parent_container = $parent_container;

		$this->init();
	}

	private function init() {
		$this->init_options();
		$this->initialize();
	}

	/**
	 * Initialization function which may be overridden if required.
	 */
	protected function initialize() {
	}

	/**
	 * This method ensures that configuration options are set on the corresponding field class instance properties. If
	 * a field has defined default options, those are set first and then can be overwritten by options which were
	 * passed in.
	 */
	private function init_options() {
		// First set properties from default options if any have been set.
		$default_field_options = $this->get_default_options();

		if ( ! empty( $default_field_options ) ) {
			foreach ( $default_field_options as $key => $value ) {
				if ( property_exists( $this, $key ) ) {
					if ( isset( $default_field_options[$key] ) ) {
						$this->$key = $value;
					}
				}
			}
		}

		$field_options = $this->field_options;

		foreach ( $field_options as $key => $value ) {
			if ( property_exists( $this, $key ) ) {
				if ( isset( $field_options[$key] ) ) {
					$this->$key = $value;
				}
			}
		}
	}

	protected function get_default_options() {
		//Stub: This function may be overridden by subclasses to have default field options.
		return null;
	}

	/**
	 * The CSS classes to be applied to the default label.
	 * This function should be overridden by subclasses when they want to add custom CSS classes to the HTML input label.
	 *
	 * @return array The array of label CSS classes.
	 */
	protected function get_label_classes( $value, $instance ) {
		return array( 'siteorigin-widget-field-label' );
	}

	/**
	 * The CSS classes to be applied to the default description.
	 * This function should be overridden by subclasses when they want to add custom CSS classes to the description text.
	 *
	 * @return array The modified array of description text CSS classes.
	 */
	protected function get_description_classes() {
		return array( 'siteorigin-widget-description' );
	}

	/**
	 * This function is called by the containing SiteOrigin_Widget when rendering it's form.
	 *
	 * @param $value mixed The current instance value of the field.
	 * @param $instance array Optionally pass in the widget instance, if rendering of additional values is required.
	 */
	public function render( $value, $instance = array() ) {
		if ( is_null( $value ) && isset( $this->default ) ) {
			$value = $this->default;
		}
		$wrapper_attributes = array(
			'class' => array(
				'siteorigin-widget-field',
				'siteorigin-widget-field-type-' . $this->type,
				'siteorigin-widget-field-' . $this->base_name,
			),
		);

		if ( ! empty( $this->optional ) ) {
			$wrapper_attributes['class'][] = 'siteorigin-widget-field-is-optional';
		}

		if ( ! empty( $this->required ) ) {
			$wrapper_attributes['class'][] = 'siteorigin-widget-field-is-required';
		}
		$wrapper_attributes['class'] = implode( ' ', array_map( 'sanitize_html_class', $wrapper_attributes['class'] ) );

		if ( ! empty( $this->state_emitter ) ) {
			// State emitters create new states for the form
			$wrapper_attributes['data-state-emitter'] = json_encode( $this->state_emitter );
		}

		if ( ! empty( $this->state_handler ) ) {
			// State handlers decide what to do with form states
			$wrapper_attributes['data-state-handler'] = json_encode( $this->state_handler );
		}

		if ( ! empty( $this->state_handler_initial ) ) {
			// Initial state handlers are only run when the form is first loaded
			$wrapper_attributes['data-state-handler-initial'] = json_encode( $this->state_handler_initial );
		}

		?><div <?php foreach ( $wrapper_attributes as $attr => $attr_val ) {
			echo esc_html( $attr ) . '="' . esc_attr( $attr_val ) . '" ';
		} ?>><?php

		// Allow subclasses and to render something before and after the render_field() function is called.
		$this->render_before_field( $value, $instance );
		$this->render_field( $value, array() );
		$this->render_after_field( $value, $instance );

		?></div><?php
	}

	/**
	 * This function is called before the main render function.
	 *
	 * @param $value mixed The current value of this field.
	 * @param $instance array The current widget instance.
	 */
	protected function render_before_field( $value, $instance ) {
		$this->render_field_label( $value, $instance );
	}

	/**
	 * Default label rendering implementation. Subclasses should override if necessary to render labels differently.
	 */
	protected function render_field_label( $value, $instance ) {
		?>
		<label for="<?php echo esc_attr( $this->element_id ); ?>" <?php $this->render_CSS_classes( $this->get_label_classes( $value, $instance ) ); ?>>
			<?php
			echo esc_html( $this->label );

			if ( ! empty( $this->optional ) ) {
				echo '<span class="field-optional">(' . esc_html__( 'Optional', 'so-widgets-bundle' ) . ')</span>';
			}

			if ( ! empty( $this->required ) ) {
				/* translators: Used to indicate field as required. */
				echo '<span class="field-required">' . esc_html__( '*', 'so-widgets-bundle' ) . '</span>';
			}
			?>
		</label>
		<?php
	}

	/**
	 * Helper function to render the HTML class attribute with the array of classes.
	 */
	protected function render_CSS_classes( $CSS_classes ) {
		if ( ! empty( $CSS_classes ) ) {
			?>class="<?php echo esc_attr( implode( ' ', array_map( 'sanitize_html_class', $CSS_classes ) ) ); ?>"<?php
		}
	}

	/**
	 * The main field rendering function. This function should be overridden by all subclasses and used to render their
	 * specific form field HTML for display.
	 *
	 * @param $value mixed The current value of this field.
	 * @param $instance array The current widget instance.
	 *
	 * @return mixed Should output the desired HTML.
	 */
	abstract protected function render_field( $value, $instance );

	/**
	 * The default sanitization function.
	 *
	 * @param $value mixed The value to be sanitized.
	 * @param $instance array The widget instance.
	 * @param $old_value mixed The old value of this field.
	 *
	 * @return mixed|string
	 */
	public function sanitize( $value, $instance = array(), $old_value = null ) {
		if ( empty( $value ) ) {
			return '';
		}

		$value = $this->sanitize_field_input( $value, $instance );

		if ( isset( $this->sanitize ) && ! empty( $value ) ) {
			// This field also needs some custom sanitization
			switch( $this->sanitize ) {
				case 'text':
					if (
						is_user_logged_in() &&
						// Fields can be sanitized for setup purposes during display.
						// As the data is sanitized during the saving purpose, we can
						// safely skip this. Not doing so could result in an error.
						! current_user_can( 'unfiltered_html' ) &&
						! apply_filters( 'siteorigin_widgets_field_allow_unfiltered_html', false )
					) {
						$value = $this->recursive_sanitize( $value );
					}
					break;
				case 'url':
					$value = sow_esc_url_raw( $value );
					break;

				case 'email':
					$value = sanitize_email( $value );
					break;

				case 'number':
					$value = (int) $value;
					break;

				default:
					// This isn't a built in sanitization. Maybe it's handled elsewhere.
					if ( is_callable( $this->sanitize ) ) {
						$value = call_user_func( $this->sanitize, $value, $old_value );
					} elseif ( is_string( $this->sanitize ) ) {
						$value = apply_filters( 'siteorigin_widgets_sanitize_field_' . $this->sanitize, $value );
					}
					break;
			}
		}

		return $value;
	}

	/**
	 * Recursively sanitizes and filters the given value using sanitize_text_field().
	 *
	 * If the value is an array, it recursively applies the sanitization to each element.
	 *
	 * @param mixed $value The value to be sanitized.
	 * @return mixed The sanitized value.
	 */
	public function recursive_sanitize( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( $this, 'recursive_sanitize' ), $value );
		}
		return sanitize_text_field( $value );
	}

	/**
	 * This function is called after the main render function.
	 *
	 * @param $value mixed The current value of this field.
	 * @param $instance array The current widget instance.
	 */
	protected function render_after_field( $value, $instance ) {
		$this->render_field_description();

		if ( ! empty( $this->required ) && is_string( $this->required ) ) {
			/* translators: Used to indicate field as required. */
			echo '<span class="field-required-message">' . esc_html( $this->required ) . '</span>';
		}
	}

	/**
	 * Default description rendering implementation. Subclasses should override if necessary to render descriptions
	 * differently.
	 */
	protected function render_field_description() {
		if ( ! empty( $this->description ) ) {
			?><div <?php $this->render_CSS_classes( $this->get_description_classes() ); ?>><?php echo wp_kses_post( $this->description ); ?></div><?php
		}
	}

	/**
	 * The main sanitization function. This function should be overridden by all subclasses and used to sanitize the
	 * input received from their HTML form field.
	 *
	 * @param $value mixed The current value of this field.
	 * @param $instance array The widget instance.
	 *
	 * @return mixed The sanitized value.
	 */
	abstract protected function sanitize_field_input( $value, $instance );

	/**
	 * There are cases where a field may affect values on the widget instance, other than it's own input. It then becomes
	 * necessary to perform additional sanitization on the widget instance, which should be done here.
	 *
	 * @return mixed
	 */
	public function sanitize_instance( $instance ) {
		//Stub: This function may be overridden by subclasses wishing to sanitize additional instance fields.
		return $instance;
	}

	/**
	 * Occasionally it is necessary for a field to set a variable to be used in the front end. Override this function
	 * and set any necessary values on the `javascript_variables` instance property.
	 *
	 * @return array
	 */
	public function get_javascript_variables() {
		return $this->javascript_variables;
	}

	/**
	 * Some more complex fields may require some JavaScript in the front end. Enqueue them here.
	 */
	public function enqueue_scripts() {
	}

	public function __get( $name ) {
		if ( isset( $this->$name ) ) {
			return $this->$name;
		}
	}
}
