<?php
if ( ! class_exists( 'GFForms' ) ) {
	die();
}

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

/**
 * Abstract Rest Controller Class
 *
 * @author   Rocketgenius
 * @category API
 * @package  Rocketgenius/Abstracts
 * @extends  WP_REST_Controller
 */
abstract class GF_REST_Controller extends WP_REST_Controller {
	/**
	 * Endpoint namespace.
	 *
	 * @since 2.4-beta-1
	 *
	 * @var string
	 */
	protected $namespace = 'gf/v2';

	/**
	 * Route base.
	 *
	 * @since 2.4-beta-1
	 *
	 * @var string
	 */
	protected $rest_base = '';

	/**
	 * Indicates if the capability validation request has been logged.
	 *
	 * Without this the other registered methods for the route will also be logged when rest_send_allow_header() in WP rest-api.php runs.
	 *
	 * @since 2.4.11
	 *
	 * @var bool
	 */
	protected $_validate_caps_logged = false;

	/**
	 * Parses the entry search, sort and paging parameters from the request
	 *
	 * @since 2.4-beta-1
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 *
	 * @return array Returns an associative array with the "search_criteria", "paging" and "sorting" keys appropriately populated.
	 */
	public function parse_entry_search_params( $request ) {

		// Sorting parameters
		$sorting_param = $request->get_param( 'sorting' );
		$sort_key = isset( $sorting_param['key'] ) && ! empty( $sorting_param['key'] ) ? $sorting_param['key'] : 'id';
		$sort_dir = isset( $sorting_param['direction'] ) && ! empty( $sorting_param['direction'] ) ? $sorting_param['direction'] : 'DESC';
		$sorting  = array( 'key' => $sort_key, 'direction' => $sort_dir );
		if ( isset( $sorting_param['is_numeric'] ) ) {
			$sorting['is_numeric'] = $sorting_param['is_numeric'];
		}

		// paging parameters
		$paging_param = $request->get_param( 'paging' );
		$page_size = isset( $paging_param['page_size'] ) ? intval( $paging_param['page_size'] ) : 10;
		if ( isset( $paging_param['current_page'] ) ) {
			$current_page = intval( $paging_param['current_page'] );
			$offset       = $page_size * ( $current_page - 1 );
		} else {
			$offset = isset( $paging_param['offset'] ) ? intval( $paging_param['offset'] ) : 0;
		}

		$paging = array( 'offset' => $offset, 'page_size' => $page_size );

		$search = $request->get_param( 'search' );
		if ( isset( $search ) ) {
			if ( ! is_array( $search ) ) {
				$search = urldecode( ( stripslashes( $search ) ) );
				$search = json_decode( $search, true );
			}
		} else {
			$search = array();
		}

		if ( ! isset( $search['status'] ) ) {
			$search['status'] = 'active';
		}

		$params = array(
			'search_criteria' => $search,
			'paging'          => $paging,
			'sorting'         => $sorting,
		);

		$form_ids = $request->get_param( 'form_ids' );

		if ( isset( $form_ids ) ) {
			$params['form_ids'] = $form_ids;
		}

		return $params;
	}

	/**
	 * JSON encodes list fields in the specified $entry and returns the new $entry
	 *
	 * @since 2.4-beta-1
	 *
	 * @param array $entry The entry object
	 *
	 * @return array Returns the $entry array with the list fields json encoded
	 */
	public function maybe_json_encode_list_fields( $entry ) {
		$form_id = $entry['form_id'];
		$form    = GFAPI::get_form( $form_id );
		if ( ! empty( $form['fields'] ) && is_array( $form['fields'] ) ) {
			foreach ( $form['fields'] as $field ) {
				/* @var GF_Field $field */
				if ( $field->get_input_type() == 'list' ) {
					$new_value = maybe_unserialize( $entry[ $field->id ] );

					if ( ! $this->is_json( $new_value ) ) {
						$new_value = json_encode( $new_value );
					}

					$entry[ $field->id ] = $new_value;
				}
			}
		}

		return $entry;
	}

	/**
	 * Determines if the specified values is a JSON encoded string
	 *
	 * @since 2.4-beta-1
	 *
	 * @param mixed $value The value to be checked
	 *
	 * @return bool True if the speficied value is JSON encoded. False otherwise
	 */
	public static function is_json( $value ) {
		if ( is_string( $value ) && in_array( substr( $value, 0, 1 ), array( '{', '[' ) ) && is_array( json_decode( $value, ARRAY_A ) ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Filters an entry, removing fields that aren't in the list of specified $field_ids
	 *
	 * @since 2.4-beta-1
	 *
	 * @param array $entry The entry to be filtered
	 * @param array $field_ids The field IDs to be kept in the entry
	 *
	 * @return array Returns the entry array, containing only the field_ids specified in the $field_ids array.
	 */
	public static function filter_entry_fields( $entry, $field_ids ) {

		if ( ! is_array( $field_ids ) ) {
			$field_ids = array( $field_ids );
		}
		$new_entry = array();
		foreach ( $entry as $key => $val ) {
			if ( in_array( $key, $field_ids ) || ( is_numeric( $key ) && in_array( intval( $key ), $field_ids ) ) ) {
				$new_entry[ $key ] = $val;
			}
		}

		return $new_entry;
	}

	/***
	 * Prepares entry for REST API response, decoding or unserializing appropriate fields
	 *
	 * @since 2.4-beta-1
	 *
	 * @param array $entry The entry array
	 *
	 * @return bool|array Returns the entry array ready to be send in the REST API response.
	 */
	public function prepare_entry_for_response( $entry ) {

		if ( is_wp_error( $entry ) || ! isset( $entry['form_id'] ) ) {
			return $entry;
		}

		$form = GFAPI::get_form( $entry['form_id'] );
		foreach ( $form['fields'] as $field ) {

			if ( empty( $entry[ $field->id ] ) ) {
				continue;
			}

			if ( $field instanceof GF_Field_MultiSelect ) {

				$entry[ $field->id ] = $field->to_array( $entry[ $field->id ] );

			} elseif ( $field instanceof GF_Field_FileUpload ) {
				$files = $field->to_array( $entry[ $field->id ] );

				$entry[ $field->id ] = $field->multipleFiles ? $files : rgar( $files, 0 );

			} elseif ( $field instanceof GF_Field_List ) {

				$entry[ $field->id ] = maybe_unserialize( $entry[ $field->id ] );

			}

		}

		return $entry;
	}

	/***
	 * Determines if the value of the specified field is stored in JSON format
	 *
	 * @since 2.4-beta-1
	 *
	 * @param GF_Field $field The field to be checked
	 *
	 * @return bool Returns true if the specified field's value is stored in JSON format. Retruns false otherwise.
	 */
	public function is_field_value_json( $field ) {

		$input_type = $field->get_input_type();

		if ( in_array( $input_type, array( 'multiselect', 'list' ) ) ) {
			return true;
		}

		if ( $field->storageType === 'json' || ( $input_type === 'fileupload' && $field->multipleFiles ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Serializes list fields in the specified $entry array.
	 *
	 * @since 2.4-beta-1
	 *
	 * @param  array $entry   The entry array
	 * @param null   $form_id The current form id
	 *
	 * @return array Returns the $entry array with all it's list fields serialized.
	 */
	public function maybe_serialize_list_fields( $entry, $form_id = null ) {
		if ( empty( $form_id ) ) {
			$form_id = $entry['form_id'];
		}
		$form = GFAPI::get_form( $form_id );
		if ( ! empty( $form['fields'] ) && is_array( $form['fields'] ) ) {
			foreach ( $form['fields'] as $field ) {
				/* @var GF_Field $field */
				if ( $field->get_input_type() == 'list' && isset( $entry[ $field->id ] ) ) {
					$new_list_value = self::maybe_decode_json( $entry[ $field->id ] );
					if ( ! is_serialized( $new_list_value ) ) {
						$new_list_value = serialize( $new_list_value );
					}
					$entry[ $field->id ] = $new_list_value;
				}
			}
		}

		return $entry;
	}

	/**
	 * JSON encodes appropriate fields in the specified $entry array
	 *
	 * @since 2.4-beta-1
	 *
	 * @param array $entry The entry array.
	 *
	 * @return array Returns the $entry array with all appropriate fields JSON encoded.
	 */
	public function maybe_json_encode_applicable_fields( $entry ) {

		$form = GFAPI::get_form( $entry['form_id'] );

		/** @var GF_Field $field */
		foreach ( $form['fields'] as $field ) {

			if ( empty( $entry[ $field->id ] ) ) {
				continue;
			}

			if ( $field instanceof GF_Field_FileUpload || $field instanceof GF_Field_MultiSelect ) {
				$entry[ $field->id ] = $field->to_string( $entry[ $field->id ] );
			}
		}

		return $entry;
	}

	/**
	 * Decodes JSON encoded strings.
	 *
	 * @since 2.4-beta-1
	 *
	 * @param string $value String to be decoded
	 *
	 * @return array|mixed Returns the decoded JSON array. If the specified $value isn't a JSON encoded string, returns
	 *                     $value.
	 */
	public static function maybe_decode_json( $value ) {
		if ( self::is_json( $value ) ) {
			return json_decode( $value, ARRAY_A );
		}

		return $value;
	}

	/**
	 * Returns the http error status
	 *
	 * @since 2.4-beta-1
	 *
	 * @param WP_Error $wp_error
	 *
	 * @return int Returns the http status recored in the specified $wp_error
	 */
	public function get_error_status( $wp_error ) {
		$error_code = $wp_error->get_error_code();
		$mappings   = array(
			'not_found'   => 404,
			'not_allowed' => 401,
		);
		$http_code  = isset( $mappings[ $error_code ] ) ? $mappings[ $error_code ] : 400;

		return $http_code;
	}

	/**
	 * Writes a message to the log
	 *
	 * @since 2.4-beta-1
	 *
	 * @param string $message
	 */
	public function log_debug( $message ) {
		GFAPI::log_debug( $message );
	}

	/**
	 * Validates that the current user has the specified capability.
	 *
	 * @since 2.4.11
	 *
	 * @param string|array    $capability The required capability.
	 * @param WP_REST_Request $request    Full data about the request.
	 *
	 * @return bool
	 */
	public function current_user_can_any( $capability, $request ) {
		$result = GFAPI::current_user_can_any( $capability );

		if ( ! $this->_validate_caps_logged ) {
			$this->log_debug( sprintf( '%s(): method: %s; route: %s; capability: %s; result: %s.', __METHOD__, $request->get_method(), $request->get_route(), json_encode( $capability ), json_encode( $result ) ) );
			$this->_validate_caps_logged = true;
		}

		return $result;
	}

	/**
	 * Recursively patches the given item with the supplied changes (deletions, updates, and additions).
	 *
	 * @since 2.4.24
	 *
	 * @param mixed $current The existing item to be modified (e.g. feed).
	 * @param mixed $changes The changes to be applied.
	 *
	 * @return mixed
	 */
	public function patch_array_recursive( $current, $changes ) {
		if ( ! $this->is_assoc_array( $changes ) ) {
			return $changes;
		}

		if ( ! $this->is_assoc_array( $current ) ) {
			$current = array();
		}

		foreach ( $changes as $key => $value ) {
			if ( is_null( $value ) ) {
				unset( $current[ $key ] );
				continue;
			}

			$current[ $key ] = $this->patch_array_recursive( rgar( $current, $key ), $value );
		}

		return $current;
	}

	/**
	 * Determines if the passed variable is an associative array.
	 *
	 * @since 2.4.24
	 *
	 * @param mixed $array The variable to be checked.
	 *
	 * @return bool
	 */
	private function is_assoc_array( $array ) {
		if ( ! is_array( $array ) ) {
			return false;
		}

		foreach ( array_keys( $array ) as $key ) {
			if ( $key !== (int) $key ) {
				return true;
			}
		}

		return false;
	}

}
