<?php

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

if ( ! class_exists( 'ACF_Local_Meta' ) ) :

	class ACF_Local_Meta {

		/** @var array Storage for meta data. */
		var $meta = array();

		/** @var mixed Storage for the current post_id. */
		var $post_id = 0;

		/**
		 * __construct
		 *
		 * Sets up the class functionality.
		 *
		 * @date    8/10/18
		 * @since   5.8.0
		 *
		 * @param   void
		 * @return  void
		 */
		function __construct() {

			// add filters
			add_filter( 'acf/pre_load_post_id', array( $this, 'pre_load_post_id' ), 1, 2 );
			add_filter( 'acf/pre_load_meta', array( $this, 'pre_load_meta' ), 1, 2 );
			add_filter( 'acf/pre_load_metadata', array( $this, 'pre_load_metadata' ), 1, 4 );
		}

		/**
		 * add
		 *
		 * Adds postmeta to storage.
		 * Accepts data in either raw or request format.
		 *
		 * @date    8/10/18
		 * @since   5.8.0
		 *
		 * @param   array   $meta    An array of metdata to store.
		 * @param   mixed   $post_id The post_id for this data.
		 * @param   boolean $is_main Makes this postmeta visible to get_field() without a $post_id value.
		 * @return  array
		 */
		function add( $meta = array(), $post_id = 0, $is_main = false ) {

			// Capture meta if supplied meta is from a REQUEST.
			if ( $this->is_request( $meta ) ) {
				$meta = $this->capture( $meta, $post_id );
			}

			// Add to storage.
			$this->meta[ $post_id ] = $meta;

			// Set $post_id reference when is the "main" postmeta.
			if ( $is_main ) {
				$this->post_id = $post_id;
			}

			// Return meta.
			return $meta;
		}

		/**
		 * is_request
		 *
		 * Returns true if the supplied $meta is from a REQUEST (serialized <form> data).
		 *
		 * @date    11/3/19
		 * @since   5.7.14
		 *
		 * @param   array $meta An array of metdata to check.
		 * @return  boolean
		 */
		function is_request( $meta = array() ) {
			return acf_is_field_key( key( $meta ) );
		}

		/**
		 * capture
		 *
		 * Returns a flattened array of meta for the given postdata.
		 * This is achieved by simulating a save whilst capturing all meta changes.
		 *
		 * @date    26/2/19
		 * @since   5.7.13
		 *
		 * @param   array $values  An array of raw values.
		 * @param   mixed $post_id The post_id for this data.
		 * @return  array
		 */
		function capture( $values = array(), $post_id = 0 ) {

			// Reset meta.
			$this->meta[ $post_id ] = array();

			// Listen for any added meta.
			add_filter( 'acf/pre_update_metadata', array( $this, 'capture_update_metadata' ), 1, 5 );

			// Simulate update.
			if ( $values ) {
				acf_update_values( $values, $post_id );
			}

			// Remove listener filter.
			remove_filter( 'acf/pre_update_metadata', array( $this, 'capture_update_metadata' ), 1, 5 );

			// Return meta.
			return $this->meta[ $post_id ];
		}

		/**
		 * capture_update_metadata
		 *
		 * Records all meta activity and returns a non null value to bypass DB updates.
		 *
		 * @date    26/2/19
		 * @since   5.7.13
		 *
		 * @param   null         $null    .
		 * @param   (int|string) $post_id The post id.
		 * @param   string       $name    The meta name.
		 * @param   mixed        $value   The meta value.
		 * @param   boolean      $hidden  If the meta is hidden (starts with an underscore).
		 * @return  false.
		 */
		function capture_update_metadata( $null, $post_id, $name, $value, $hidden ) {
			$name                            = ( $hidden ? '_' : '' ) . $name;
			$this->meta[ $post_id ][ $name ] = $value;

			// Return non null value to escape update process.
			return true;
		}

		/**
		 * remove
		 *
		 * Removes postmeta from storage.
		 *
		 * @date    8/10/18
		 * @since   5.8.0
		 *
		 * @param   mixed $post_id The post_id for this data.
		 * @return  void
		 */
		function remove( $post_id = 0 ) {

			// unset meta
			unset( $this->meta[ $post_id ] );

			// reset post_id
			if ( $post_id === $this->post_id ) {
				$this->post_id = 0;
			}
		}

		/**
		 * pre_load_meta
		 *
		 * Injects the local meta.
		 *
		 * @date    8/10/18
		 * @since   5.8.0
		 *
		 * @param   null  $null    An empty parameter. Return a non null value to short-circuit the function.
		 * @param   mixed $post_id The post_id for this data.
		 * @return  mixed
		 */
		function pre_load_meta( $null, $post_id ) {
			if ( isset( $this->meta[ $post_id ] ) ) {
				return $this->meta[ $post_id ];
			}
			return $null;
		}

		/**
		 * pre_load_metadata
		 *
		 * Injects the local meta.
		 *
		 * @date    8/10/18
		 * @since   5.8.0
		 *
		 * @param   null         $null    An empty parameter. Return a non null value to short-circuit the function.
		 * @param   (int|string) $post_id The post id.
		 * @param   string       $name    The meta name.
		 * @param   boolean      $hidden  If the meta is hidden (starts with an underscore).
		 * @return  mixed
		 */
		function pre_load_metadata( $null, $post_id, $name, $hidden ) {
			$name = ( $hidden ? '_' : '' ) . $name;
			if ( isset( $this->meta[ $post_id ] ) ) {
				if ( isset( $this->meta[ $post_id ][ $name ] ) ) {
					return $this->meta[ $post_id ][ $name ];
				}
				return '__return_null';
			}
			return $null;
		}

		/**
		 * pre_load_post_id
		 *
		 * Injects the local post_id.
		 *
		 * @date    8/10/18
		 * @since   5.8.0
		 *
		 * @param   null  $null    An empty parameter. Return a non null value to short-circuit the function.
		 * @param   mixed $post_id The post_id for this data.
		 * @return  mixed
		 */
		function pre_load_post_id( $null, $post_id ) {
			if ( ! $post_id && $this->post_id ) {
				return $this->post_id;
			}
			return $null;
		}
	}

endif; // class_exists check

/**
 * acf_setup_meta
 *
 * Adds postmeta to storage.
 *
 * @date    8/10/18
 * @since   5.8.0
 * @see     ACF_Local_Meta::add() for list of parameters.
 *
 * @return  array
 */
function acf_setup_meta( $meta = array(), $post_id = 0, $is_main = false ) {
	return acf_get_instance( 'ACF_Local_Meta' )->add( $meta, $post_id, $is_main );
}

/**
 * acf_reset_meta
 *
 * Removes postmeta to storage.
 *
 * @date    8/10/18
 * @since   5.8.0
 * @see     ACF_Local_Meta::remove() for list of parameters.
 *
 * @return  void
 */
function acf_reset_meta( $post_id = 0 ) {
	return acf_get_instance( 'ACF_Local_Meta' )->remove( $post_id );
}
