<?php // phpcs:ignore WordPress.Files.FileName
/**
 * Abstract Data.
 *
 * This class load the data that will use in the subclasses
 *
 * @class YWDPD_Data
 * @version 3.0.0
 * @package YITH WooCommerce Dynamic Prcing and Discounts\Classes
 */

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

/**
 * Abstract YWDPD_Data Class
 *
 * Implemented by classes using the same CRUD(s) pattern.
 *
 * @version 3.0.0
 * @package YITH WooCommerce Dynamic Prcing and Discount\Classes
 */
#[AllowDynamicProperties]
abstract class YWDPD_Data implements ArrayAccess {

	/**
	 * ID for this object.
	 *
	 * @since 3.0.0
	 * @var int
	 */
	protected $id;

	/**
	 * Core data for this object. Name value pairs ( name + default value ).
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array();

	/**
	 * Extra data for this object. Name value pairs (name + default value).
	 * Used as a standard way for sub classes (like product types) to add
	 * additional information to an inherited class.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $extra_data = array();

	/**
	 * The default data
	 *
	 * @var array
	 */
	protected $default_data = array();

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'ywdpd_discount';

	/**
	 * This is the name of this object type.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	protected $object_type = 'cpt_object';

	/**
	 * This is false until the object is read from the DB.
	 *
	 * @since 3.0.0
	 * @var bool
	 */
	protected $object_read = false;

	/**
	 * This array will contain the special meta key.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $special_meta = array();

	/**
	 * Allow to add extra meta in the object.
	 *
	 * @var array
	 */
	protected $meta_data = array();

	/**
	 * Default constructor.
	 *
	 * @param int|object|array $obj ID to load from the DB (optional) or already queried data.
	 */
	public function __construct( $obj = 0 ) {
		$this->data         = array_merge( $this->data, $this->extra_data );
		$this->default_data = $this->data;
	}

	// SETTER METHODS .

	/**
	 * Set the ID.
	 *
	 * @param int $id ID.
	 *
	 * @since 3.0.0
	 */
	public function set_id( $id ) {
		$this->id = absint( $id );
	}

	/**
	 * Set object read property.
	 *
	 * @param boolean $read Should read?.
	 *
	 * @since 3.0.0
	 */
	public function set_object_read( $read = true ) {
		$this->object_read = (bool) $read;
	}

	/**
	 * Sets a prop for a setter method.
	 *
	 * This stores changes in a special array so we can track what needs saving
	 * the the DB later.
	 *
	 * @param string $prop Name of prop to set.
	 * @param mixed  $value Value of the prop.
	 *
	 * @since 3.0.0
	 */
	protected function set_prop( $prop, $value ) {
		if ( array_key_exists( $prop, $this->data ) ) {

			$this->data[ $prop ] = $value;

		}
	}

	/**
	 * Set all props to default values.
	 *
	 * @since 3.0.0
	 */
	public function set_defaults() {
		$this->data    = $this->default_data;
		$this->changes = array();
		$this->set_object_read( false );
	}

	// GETTER METHODS.

	/**
	 * Get object read property.
	 *
	 * @return boolean
	 * @since  3.0.0
	 */
	public function get_object_read() {
		return (bool) $this->object_read;
	}

	/**
	 * Returns all data for this object.
	 *
	 * @return array
	 * @since  2.6.0
	 */
	public function get_data() {
		return array_merge( array( 'id' => $this->get_id() ), $this->data );
	}

	/**
	 * Returns array of expected data keys for this object.
	 *
	 * @return array
	 * @since   3.0.0
	 */
	public function get_data_keys() {
		return array_keys( $this->data );
	}

	/**
	 * Returns all "extra" data keys for an object (for sub objects like product types).
	 *
	 * @return array
	 * @since  3.0.0
	 */
	public function get_extra_data_keys() {
		return array_keys( $this->extra_data );
	}

	/**
	 * Get the right meta name from the prop name.
	 *
	 * @param string $prop The name.
	 *
	 * @return string
	 * @since 3.0.0
	 */
	protected function get_meta_by_prop( $prop ) {
		if ( ! in_array( $prop, $this->special_meta, true ) ) {
			return '_' . $prop;
		} else {
			return $prop;
		}
	}

	/**
	 * Returns the unique ID for this object.
	 *
	 * @return int
	 * @since  3.0.0
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Prefix for action and filter hooks on data.
	 *
	 * @return string
	 * @since  3.0.0
	 */
	protected function get_hook_prefix() {
		return 'ywdpd_' . $this->object_type . '_get_';
	}

	/**
	 * Gets a prop for a getter method.
	 *
	 * Gets the value from either current pending changes, or the data itself.
	 * Context controls what happens to the value before it's returned.
	 *
	 * @param string $prop Name of prop to get.
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return mixed
	 * @since  3.0.0
	 */
	protected function get_prop( $prop, $context = 'view' ) {
		$value = null;

		if ( array_key_exists( $prop, $this->data ) ) {
			$value = $this->data[ $prop ];

			if ( 'view' === $context ) {
				/**
				 * APPLY_FILTERS: ywdpd_$OBJECT_TYPE_get_$PROP
				 *
				 * This filter change the value of an object property.
				 *
				 * @param mixed      $value The value to filter.
				 * @param YWDPD_Data $object The current object.
				 *
				 * @return mixed
				 */

				$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
			}
		}

		return $value;
	}

	/**
	 * Get the post status of this object.
	 *
	 * @return string
	 * @since 3.0.0
	 */
	public function get_post_status() {
		return get_post_status( $this->get_id() );
	}

	// OTHER METHODS.

	/**
	 * Only store the object ID to avoid serializing the data object instance.
	 *
	 * @return array
	 */
	public function __sleep() {
		return array( 'id' );
	}

	/**
	 * Re-run the constructor with the object ID.
	 *
	 * If the object no longer exists, remove the ID.
	 */
	public function __wakeup() {
		try {
			$this->__construct( absint( $this->id ) );
		} catch ( Exception $e ) {
			$this->set_id( 0 );
			$this->set_object_read( true );
		}
	}

	/**
	 * Change data to JSON format.
	 *
	 * @return string Data in JSON format.
	 * @since  2.6.0
	 */
	public function __toString() {
		return wp_json_encode( $this->get_data() );
	}

	/**
	 * Return external meta
	 *
	 * @param string $key The meta key.
	 *
	 * @return mixed
	 * @author YITH <plugins@yithemes.com>
	 * @since 3.0.0
	 */
	public function get_meta( $key ) {

		if ( isset( $this->meta_data[ $key ] ) ) {
			$value = $this->meta_data[ $key ];
		} else {
			$value                   = maybe_unserialize( get_post_meta( $this->get_id(), '_' . $key, true ) );
			$this->meta_data[ $key ] = $value;
		}

		return $value;
	}

	/**
	 * Offset Set
	 *
	 * @param mixed $offset The offset.
	 * @param mixed $value The value.
	 *
	 * @since 3.0.0
	 */
	#[\ReturnTypeWillChange]
	public function offsetSet( $offset, $value ) {
		/* do nothing */
	}

	/**
	 * Check if offset exist
	 *
	 * @param mixed $offset The offset.
	 *
	 * @return bool
	 * @since 3.0.0
	 */
	#[\ReturnTypeWillChange]
	public function offsetExists( $offset ) {
		$getter = 'get_' . $offset;

		if ( is_callable( array( $this, $getter ) ) ) {
			return true;
		} else {
			return metadata_exists( 'post', $this->get_id(), '_' . $offset );
		}
	}

	/**
	 * Offset Unset
	 *
	 * @param mixed $offset The offset.
	 *
	 * @since 3.0.0
	 */
	#[\ReturnTypeWillChange]
	public function offsetUnset( $offset ) {
		/* do nothing */
	}

	/**
	 * Get the offset value
	 *
	 * @param mixed $offset The offset.
	 *
	 * @return mixed|null
	 * @since 3.0.0
	 */
	#[\ReturnTypeWillChange]
	public function offsetGet( $offset ) {
		$getter = 'get_' . $offset;

		if ( is_callable( array( $this, $getter ) ) ) {
			$value = $this->$getter();
		} else {
			$value = $this->get_meta( $offset );
		}

		return $value;
	}
}
