<?php

/**
 * Please do not use this class directly.
 * You should instead extend it per-field-type.
 */
class Mageewp_Field {

	/**
	 * The ID of the mageewp_config we're using.
	 *
	 * @see Mageewp_Config
	 * @access protected
	 * @var string
	 */
	protected $mageewp_config = 'global';

	/**
	 * Thje capability required so that users can edit this field.
	 *
	 * @access protected
	 * @var string
	 */
	protected $capability = 'edit_theme_options';

	/**
	 * If we're using options instead of theme_mods
	 * and we want them serialized, this is the option that
	 * will saved in the db.
	 *
	 * @access protected
	 * @var string
	 */
	protected $option_name = '';

	/**
	 * Custom input attributes (defined as an array).
	 *
	 * @access protected
	 * @var array
	 */
	protected $input_attrs = array();

	/**
	 * Use "theme_mod" or "option".
	 *
	 * @access protected
	 * @var string
	 */
	protected $option_type = 'theme_mod';

	/**
	 * The name of this setting (id for the db).
	 *
	 * @access protected
	 * @var string|array
	 */
	protected $settings = '';

	/**
	 * Set to true if you want to disable all CSS output for this field.
	 *
	 * @access protected
	 * @var bool
	 */
	protected $disable_output = false;

	/**
	 * The field type.
	 *
	 * @access protected
	 * @var string
	 */
	protected $type = 'mageewp-generic';

	/**
	 * Some fields require options to be set.
	 * We're whitelisting the property here
	 * and suggest you validate this in a child class.
	 *
	 * @access protected
	 * @var array
	 */
	protected $choices = array();

	/**
	 * Assign this field to a section.
	 * Fields not assigned to a section will not be displayed in the customizer.
	 *
	 * @access protected
	 * @var string
	 */
	protected $section = '';

	/**
	 * The default value for this field.
	 *
	 * @access protected
	 * @var string|array
	 */
	protected $default = '';

	/**
	 * Priority determines the position of a control inside a section.
	 * Lower priority numbers move the control to the top.
	 *
	 * @access protected
	 * @var int
	 */
	protected $priority = 10;

	/**
	 * Unique ID for this field.
	 * This is auto-calculated from the $settings argument.
	 *
	 * @access protected
	 * @var string
	 */
	protected $id = '';

	/**
	 * Use if you want to automatically generate CSS from this field's value.
	 *
	 * @see https://hoo.org/docs/arguments/output
	 * @access protected
	 * @var array
	 */
	protected $output = array();

	/**
	 * Use to automatically generate postMessage scripts.
	 * Not necessary to use if you use 'transport' => 'auto'
	 * and have already set an array for the 'output' argument.
	 *
	 * @see https://hoo.org/docs/arguments/js_vars
	 * @access protected
	 * @var array
	 */
	protected $js_vars = array();

	/**
	 * If you want to use a CSS compiler, then use this to set the variable names.
	 *
	 * @see https://hoo.org/docs/arguments/variables
	 * @access protected
	 * @var array
	 */
	protected $variables = array();

	/**
	 * Text that will be used in a tooltip to provide extra info for this field.
	 *
	 * @access protected
	 * @var string
	 */
	protected $tooltip = '';

	/**
	 * Whitelisting for backwards-compatibility.
	 *
	 * @access protected
	 * @var string
	 */
	protected $help = '';

	/**
	 * A custom callback to determine if the field should be visible or not.
	 *
	 * @access protected
	 * @var string|array
	 */
	protected $active_callback = '__return_true';

	/**
	 * A custom sanitize callback that will be used to properly save the values.
	 *
	 * @access protected
	 * @var string|array
	 */
	protected $sanitize_callback = '';

	/**
	 * Use 'refresh', 'postMessage' or 'auto'.
	 * 'auto' will automatically geberate any 'js_vars' from the 'output' argument.
	 *
	 * @access protected
	 * @var string
	 */
	protected $transport = 'refresh';

	/**
	 * Define dependencies to show/hide this field based on the values of other fields.
	 *
	 * @access protected
	 * @var array
	 */
	protected $required = array();

	/**
	 * Suggested width for cropped image.
	 *
	 * @access protected
	 * @var int
	 */
	protected $width = 150;

	/**
	 * Suggested height for cropped image.
	 *
	 * @access protected
	 * @var int
	 */
	protected $height = 150;

	/**
	 * Whether the width is flexible for cropped image.
	 *
	 * @access protected
	 * @var bool
	 */
	protected $flex_width = false;

	/**
	 * Whether the height is flexible for cropped image.
	 *
	 * @access protected
	 * @var bool
	 */
	protected $flex_height = false;

	/**
	 * Contain the settings for the repeater rows labels
	 *
	 * @access protected
	 * @var array
	 */
	protected $row_label = array();

	/**
	 * Partial Refreshes array.
	 *
	 * @access protected
	 * @var array
	 */
	protected $partial_refresh = array();

	/**
	 * Use only on image, cropped_image, upload controls.
	 * Limit the Media library to a specific mime type
	 *
	 * @access protected
	 * @var array
	 */
	protected $mime_type = '';

	/**
	 * Used by image fields.
	 *
	 * @access protected
	 * @var array
	 */
	protected $button_labels = array();

	/**
	 * Use only on select controls.
	 * Defines if this is a multi-select or not.
	 * If value is > 1, then the maximum number of selectable options
	 * is the number defined here.
	 *
	 * @access protected
	 * @var integer
	 */
	protected $multiple = 1;

	/**
	 * Allows fields to be collapsible.
	 *
	 * @access protected
	 * @var bool
	 */
	protected $collapsible = false;

	/**
	 * The class constructor.
	 * Parses and sanitizes all field arguments.
	 * Then it adds the field to Mageewp::$fields.
	 *
	 * @access public
	 * @param string $config_id    The ID of the config we want to use.
	 *                             Defaults to "global".
	 *                             Configs are handled by the Mageewp_Config class.
	 * @param array  $args         The arguments of the field.
	 */
	public function __construct( $config_id = 'global', $args = array() ) {

		if ( isset( $args['setting'] ) && ! empty( $args['setting'] ) && ( ! isset( $args['settings'] ) || empty( $args['settings'] ) ) ) {
			$args['settings'] = $args['setting'];
			unset( $args['setting'] );
			error_log( 'Mageewp: Typo found in field ' . $args['settings'] . ' ("setting" instead of "settings").' );
		}

		// In case the user only provides 1 argument,
		// assume that the provided argument is $args and set $config_id = 'global'.
		if ( is_array( $config_id ) && empty( $args ) ) {
			$args = $config_id;
			$config_id = 'global';
		}

		$args['mageewp_config'] = $config_id;

		$this->mageewp_config = ( '' === $config_id ) ? 'global' : trim( esc_attr( $config_id ) );

		// Get defaults from the class.
		$defaults = get_class_vars( __CLASS__ );

		// Get the config arguments, and merge them with the defaults.
		$config_defaults = ( isset( Mageewp::$config['global'] ) ) ? Mageewp::$config['global'] : array();
		if ( 'global' !== $this->mageewp_config && isset( Mageewp::$config[ $this->mageewp_config ] ) ) {
			$config_defaults = Mageewp::$config[ $this->mageewp_config ];
		}
		$config_defaults = ( is_array( $config_defaults ) ) ? $config_defaults : array();
		foreach ( $config_defaults as $key => $value ) {
			if ( isset( $defaults[ $key ] ) && ! empty( $value ) && $value != $defaults[ $key ] ) {
				$defaults[ $key ] = $value;
			}
		}

		// Merge our args with the defaults.
		$args = wp_parse_args( $args, $defaults );

		// Set the class properties using the parsed args.
		foreach ( $args as $key => $value ) {
			$this->$key = $value;
		}

		// An array of whitelisted properties that don't need to be sanitized here.
		// Format: $key => $default_value.
		$whitelisted = apply_filters( "options-framework/{$this->mageewp_config}/fields/properties_whitelist", array(
			'label'       => '', // This is sanitized later in the controls themselves.
			'description' => '', // This is sanitized later in the controls themselves.
			'mode'        => '', // Only used for backwards-compatibility reasons.
			'fields'      => array(), // Used in repeater fields.
			'row_label'   => array(), // Used in repeater fields.
		) );

		$this->set_field( $whitelisted );

	}

	/**
	 * Processes the field arguments
	 *
	 * @param array $whitelisted_properties Defines an array of arguments that will skip validation at this point.
	 */
	protected function set_field( $whitelisted_properties = array() ) {

		$properties = get_class_vars( __CLASS__ );
		// Remove any whitelisted properties from above.
		// These will get a free pass, completely unfiltered.
		foreach ( $whitelisted_properties as $key => $default_value ) {
			if ( isset( $properties[ $key ] ) ) {
				unset( $properties[ $key ] );
			}
		}

		// Some things must run before the others.
		$priorities = array(
			'option_name',
			'option_type',
			'settings',
		);

		foreach ( $priorities as $priority ) {
			if ( method_exists( $this, 'set_' . $priority ) ) {
				$method_name = 'set_' . $priority;
				$this->$method_name();
			}
		}

		// Sanitize the properties, skipping the ones run from the $priorities.
		foreach ( $properties as $property => $value ) {
			if ( in_array( $property, $priorities, true ) ) {
				continue;
			}
			if ( method_exists( $this, 'set_' . $property ) ) {
				$method_name = 'set_' . $property;
				$this->$method_name();
			}
		}

		// Get all arguments with their values.
		$args = get_object_vars( $this );
		foreach ( $args as $key => $default_value ) {
			$args[ $key ] = $this->$key;
		}

		// Add the whitelisted properties through the back door.
		foreach ( $whitelisted_properties as $key => $default_value ) {
			if ( ! isset( $this->$key ) ) {
				$this->$key = $default_value;
			}
			$args[ $key ] = $this->$key;
		}

		// Add the field to the static $fields variable properly indexed.
		Mageewp::$fields[ $this->settings ] = $args;

	}

	/**
	 * This allows us to process this on a field-basis
	 * by using sub-classes which can override this method.
	 *
	 * @access protected
	 */
	protected function set_default() {}

	/**
	 * Escape $mageewp_config.
	 *
	 * @access protected
	 */
	protected function set_mageewp_config() {

		$this->mageewp_config = esc_attr( $this->mageewp_config );

	}

	/**
	 * Escape $option_name.
	 *
	 * @access protected
	 */
	protected function set_option_name() {

		$this->option_name = esc_attr( $this->option_name );

	}

	/**
	 * Escape the $section.
	 *
	 * @access protected
	 */
	protected function set_section() {

		$this->section = sanitize_key( $this->section );

	}

	/**
	 * Escape the $section.
	 *
	 * @access protected
	 */
	protected function set_input_attrs() {

		if ( ! is_array( $this->input_attrs ) ) {
			$this->input_attrs = array();
		}
	}

	/**
	 * Checks the capability chosen is valid.
	 * If not, then falls back to 'edit_theme_options'
	 *
	 * @access protected
	 */
	protected function set_capability() {

		// Early exit if we're using 'edit_theme_options'.
		if ( 'edit_theme_options' === $this->capability ) {
			return;
		}
		// Escape & trim the capability.
		$this->capability = trim( esc_attr( $this->capability ) );

	}

	/**
	 * Make sure we're using the correct option_type
	 *
	 * @access protected
	 */
	protected function set_option_type() {

		// Take care of common typos.
		if ( 'options' === $this->option_type ) {
			$this->option_type = 'option';
		}
		// Take care of common typos.
		if ( 'theme_mods' === $this->option_type ) {
			$this->option_type = 'theme_mod';
		}
	}

	/**
	 * Modifications for partial refreshes.
	 *
	 * @access protected
	 */
	protected function set_partial_refresh() {

		if ( ! is_array( $this->partial_refresh ) ) {
			$this->partial_refresh = array();
		}
		foreach ( $this->partial_refresh as $id => $args ) {
			if ( ! is_array( $args ) || ! isset( $args['selector'] ) || ! isset( $args['render_callback'] ) || ! is_callable( $args['render_callback'] ) ) {
				unset( $this->partial_refresh[ $id ] );
				continue;
			}
		}
		if ( ! empty( $this->partial_refresh ) ) {
			$this->transport = 'postMessage';
		}
	}

	/**
	 * Sets the settings.
	 * If we're using serialized options it makes sure that settings are properly formatted.
	 * We'll also be escaping all setting names here for consistency.
	 *
	 * @access protected
	 */
	protected function set_settings() {

		// If settings is not an array, temporarily convert it to an array.
		// This is just to allow us to process everything the same way and avoid code duplication.
		// if settings is not an array then it will not be set as an array in the end.
		if ( ! is_array( $this->settings ) ) {
			$this->settings = array(
				'mageewp_placeholder_setting' => $this->settings,
			);
		}
		$settings = array();
		foreach ( $this->settings as $setting_key => $setting_value ) {
			$settings[ sanitize_key( $setting_key ) ] = esc_attr( $setting_value );
			// If we're using serialized options then we need to spice this up.
			if ( 'option' === $this->option_type && '' !== $this->option_name && ( false === strpos( $setting_key, '[' ) ) ) {
				$settings[ sanitize_key( $setting_key ) ] = esc_attr( $this->option_name ) . '[' . esc_attr( $setting_value ) . ']';
			}
		}
		$this->settings = $settings;
		if ( isset( $this->settings['mageewp_placeholder_setting'] ) ) {
			$this->settings = $this->settings['mageewp_placeholder_setting'];
		}

	}

	/**
	 * Escapes the tooltip messages.
	 *
	 * @access protected
	 */
	protected function set_tooltip() {

		if ( '' !== $this->tooltip ) {
			$this->tooltip = wp_strip_all_tags( $this->tooltip );
			return;
		}
	}

	/**
	 * Sets the active_callback
	 * If we're using the $required argument,
	 * Then this is where the switch is made to our evaluation method.
	 *
	 * @access protected
	 */
	protected function set_active_callback() {

		if ( is_array( $this->active_callback ) && ! is_callable( $this->active_callback ) ) {
			if ( isset( $this->active_callback[0] ) ) {
				$this->required = $this->active_callback;
			}
		}

		if ( ! empty( $this->required ) ) {
			$this->active_callback = array( 'Mageewp_Active_Callback', 'evaluate' );
			return;
		}
		// No need to proceed any further if we're using the default value.
		if ( '__return_true' === $this->active_callback ) {
			return;
		}
		// Make sure the function is callable, otherwise fallback to __return_true.
		if ( ! is_callable( $this->active_callback ) ) {
			$this->active_callback = '__return_true';
		}
	}

	/**
	 * Sets the control type.
	 *
	 * @access protected
	 */
	protected function set_type() {

		// Escape the control type (it doesn't hurt to be sure).
		$this->type = esc_attr( $this->type );

	}

	/**
	 * Sets the $id.
	 * Setting the ID should happen after the 'settings' sanitization.
	 * This way we can also properly handle cases where the option_type is set to 'option'
	 * and we're using an array instead of individual options.
	 *
	 * @access protected
	 */
	protected function set_id() {

		$this->id = sanitize_key( str_replace( '[', '-', str_replace( ']', '', $this->settings ) ) );

	}

	/**
	 * Sets the $sanitize_callback
	 *
	 * @access protected
	 */
	protected function set_sanitize_callback() {

		// If a custom sanitize_callback has been defined,
		// then we don't need to proceed any further.
		if ( ! empty( $this->sanitize_callback ) ) {
			return;
		}

		$default_callbacks = array(
			'mageewp-multicheck'       => array( 'Mageewp_Sanitize_Values', 'multicheck' ),
			'mageewp-typography'       => array( 'Mageewp_Sanitize_Values', 'typography' ),
		);

		if ( array_key_exists( $this->type, $default_callbacks ) ) {
			$this->sanitize_callback = $default_callbacks[ $this->type ];
		}

	}

	/**
	 * Sets the $choices.
	 *
	 * @access protected
	 */
	protected function set_choices() {

		if ( ! is_array( $this->choices ) ) {
			$this->choices = array();
		}
	}

	/**
	 * Escapes the $disable_output.
	 *
	 * @access protected
	 */
	protected function set_disable_output() {

		$this->disable_output = (bool) $this->disable_output;

	}

	/**
	 * Sets the $sanitize_callback
	 *
	 * @access protected
	 */
	protected function set_output() {

		if ( empty( $this->output ) ) {
			return;
		}
		if ( ! empty( $this->output ) && ! is_array( $this->output ) ) {
			$this->output = array(
				array(
					'element' => $this->output,
				),
			);
		}
		// Convert to array of arrays if needed.
		if ( isset( $this->output['element'] ) ) {
			$this->output = array( $this->output );
		}
		$outputs = array();
		foreach ( $this->output as $output ) {
			if ( ! isset( $output['element'] ) || ( ! isset( $output['property'] ) && ! in_array( $this->type, array( 'mageewp-typography', 'mageewp-background' ), true ) ) ) {
				continue;
			}
			if ( ! isset( $output['sanitize_callback'] ) && isset( $output['callback'] ) ) {
				$output['sanitize_callback'] = $output['callback'];
			}
			// Convert element arrays to strings.
			if ( is_array( $output['element'] ) ) {
				$output['element'] = array_unique( $output['element'] );
				sort( $output['element'] );
				$output['element'] = implode( ',', $output['element'] );
			}
			$outputs[] = array(
				'element'           => $output['element'],
				'property'          => ( isset( $output['property'] ) ) ? $output['property'] : '',
				'media_query'       => ( isset( $output['media_query'] ) ) ? $output['media_query'] : 'global',
				'sanitize_callback' => ( isset( $output['sanitize_callback'] ) ) ? $output['sanitize_callback'] : '',
				'units'             => ( isset( $output['units'] ) ) ? $output['units'] : '',
				'prefix'            => ( isset( $output['prefix'] ) ) ? $output['prefix'] : '',
				'suffix'            => ( isset( $output['suffix'] ) ) ? $output['suffix'] : '',
				'exclude'           => ( isset( $output['exclude'] ) ) ? $output['exclude'] : false,
			);
		}
	}

	/**
	 * Sets the $js_vars
	 *
	 * @access protected
	 */
	protected function set_js_vars() {

		if ( ! is_array( $this->js_vars ) ) {
			$this->js_vars = array();
		}

		// Check if transport is set to auto.
		// If not, then skip the auto-calculations and exit early.
		if ( 'auto' !== $this->transport ) {
			return;
		}

		// Set transport to refresh initially.
		// Serves as a fallback in case we failt to auto-calculate js_vars.
		$this->transport = 'refresh';

		$js_vars = array();

		// Try to auto-generate js_vars.
		// First we need to check if js_vars are empty, and that output is not empty.
		if ( empty( $this->js_vars ) && ! empty( $this->output ) ) {

			// Start going through each item in the $output array.
			foreach ( $this->output as $output ) {
				$output['function'] = ( isset( $output['function'] ) ) ? $output['function'] : 'style';

				// If 'element' or 'property' are not defined, skip this.
				if ( ! isset( $output['element'] ) || ! isset( $output['property'] ) ) {
					continue;
				}
				if ( is_array( $output['element'] ) ) {
					$output['element'] = implode( ',', $output['element'] );
				}

				// If there's a sanitize_callback defined skip this, unless we also have a js_callback defined.
				if ( isset( $output['sanitize_callback'] ) && ! empty( $output['sanitize_callback'] ) && ! isset( $output['js_callback'] ) ) {
					continue;
				}

				// If we got this far, it's safe to add this.
				$js_vars[] = $output;
			}

			// Did we manage to get all the items from 'output'?
			// If not, then we're missing something so don't add this.
			if ( count( $js_vars ) !== count( $this->output ) ) {
				return;
			}
			$this->js_vars   = $js_vars;
			$this->transport = 'postMessage';

		}
	}

	/**
	 * Sets the $variables
	 *
	 * @access protected
	 */
	protected function set_variables() {

		if ( ! is_array( $this->variables ) ) {
			$variable = ( is_string( $this->variables ) && ! empty( $this->variables ) ) ? $this->variables : false;
			$this->variables = array();
			if ( $variable && empty( $this->variables ) ) {
				$this->variables[0]['name'] = $variable;
			}
		}
	}

	/**
	 * This is a fallback method:
	 * $help has now become $tooltip, so this just migrates the data
	 *
	 * @access protected
	 */
	protected function set_help() {

		if ( '' !== $this->tooltip ) {
			return;
		}
		if ( '' !== $this->help ) {
			$this->tooltip = wp_strip_all_tags( $this->help );
			// $help has been deprecated
			$this->help = '';
			return;
		}
	}

	/**
	 * Sets the $transport
	 *
	 * @access protected
	 */
	protected function set_transport() {

		if ( 'postmessage' === trim( strtolower( $this->transport ) ) ) {
			$this->transport = 'postMessage';
		}
	}

	/**
	 * Sets the $required
	 *
	 * @access protected
	 */
	protected function set_required() {

		if ( ! is_array( $this->required ) ) {
			$this->required = array();
		}
	}

	/**
	 * Sets the $priority
	 *
	 * @access protected
	 */
	protected function set_priority() {

		$this->priority = absint( $this->priority );

	}

	/**
	 * Sets the $collapsible var.
	 *
	 * @access protected
	 */
	protected function set_collapsible() {

		$this->collapsible = (bool) $this->collapsible;

	}
}
