<?php
/**
 * Base model, migrated from Pro
 *
 * @link       https://personalizewp.com/
 * @since      3.2.0 Merged into PWP
 * @since      1.0.0
 *
 * @package    PersonalizeWP
 * @subpackage PersonalizeWP/Model
 */

namespace PersonalizeWP\Model;

defined( 'ABSPATH' ) || exit; // Exit if accessed directly.

/**
 * Base model
 *
 * This class defines base model functionality
 */
abstract class Model {

	/**
	 * Database ID
	 *
	 * @var int
	 */
	protected int $ID = 0;

	/**
	 * Database table name
	 *
	 * @var string
	 */
	protected static string $table = '';

	/**
	 * Data for model instance
	 *
	 * @var array
	 */
	protected $data = [];

	/**
	 * Attributes for model, matching database columns
	 *
	 * @var array
	 */
	protected static $attributes = [];

	/**
	 * Constructor
	 *
	 * @param array $data Data to validate against attributes
	 */
	public function __construct( $data = [] ) {

		if ( empty( $data ) ) {
			return;
		}

		foreach ( $data as $property => $value ) {
			if ( 'ID' === strtoupper( $property ) ) {
				$this->ID = (int) $value;
			}

			// Only store that which is expected.
			if ( in_array( $property, static::$attributes, true ) ) {
				$this->data[ $property ] = $value;
			}
		}
	}

	/**
	 * Proxies getting values for object properties
	 *
	 * @param  string $property Object property
	 * @return mixed
	 */
	public function __get( $property ) {

		if ( 'ID' === $property ) {
			return $this->ID;
		}

		// Allow returning all data at once.
		if ( 'data' === $property ) {
			return $this->data;
		}

		// Check for db based data via defined attributes.
		if ( in_array( $property, static::$attributes, true ) && array_key_exists( $property, $this->data ) ) {
			return $this->data[ $property ];
		}

		// Allow for custom attribute getters
		$attr_method_name = 'get' . str_replace( '_', '', ucwords( $property, '_' ) ) . 'Attribute';

		if ( method_exists( $this, $attr_method_name ) ) {

			if ( ! array_key_exists( $property, $this->data ) ) {
				// Otherwise get and store results of method as non-persistent cache on object.
				$this->data[ $property ] = $this->$attr_method_name();

			}
			// ToDo: Update cache?

			return $this->data[ $property ];
		}

		return null;
	}

	/**
	 * Proxies setting values for object properties
	 * Validates against known model attributes
	 *
	 * @param string $property Property to store data against
	 * @param mixed  $value    Value for property
	 *
	 * @return void
	 */
	public function __set( $property, $value ) {

		// Only update that which is expected, silently ignore the rest.
		if ( in_array( $property, static::$attributes, true ) ) {
			$this->data[ $property ] = $value;
		}
	}

	/**
	 * Used by PHP for isset() and empty() checks
	 *
	 * @param  string $property Object property
	 *
	 * @return bool
	 */
	public function __isset( $property ) {
		return isset( $this->data[ $property ] );
	}

	/**
	 * Save Model to database
	 *
	 * @return bool|integer Database ID on success, false otherwise
	 */
	public function save() {
		global $wpdb;

		$table = static::$table;

		// Filter obj data to what is needed for the database, removing cached data/non-db attributes.
		$data = array_filter(
			$this->data,
			function ( $key ) {
				return in_array( $key, static::$attributes, true );
			},
			ARRAY_FILTER_USE_KEY
		);

		if ( $this->ID ) {
			$wpdb->update( "{$wpdb->prefix}{$table}", $data, [ 'ID' => $this->ID ] );

			// Cache always includes the ID.
			$data['ID'] = $this->ID;

			// Set cache of item.
			wp_cache_set( $this->ID, $data, $table );

		} else {
			// If the table supports the `created` column, and it's not set, set it.
			if ( in_array( 'created', static::$attributes, true ) && empty( $data['created'] ) ) {
				$data['created'] = current_time( 'mysql' );
			}
			$result = $wpdb->insert( "{$wpdb->prefix}{$table}", $data );
			if ( ! $result ) {
				return false;
			}
			$this->ID = (int) $wpdb->insert_id;

			// Cache always includes the ID.
			$data['ID'] = $this->ID;

			wp_cache_add( $this->ID, $data, $table );
		}

		return $this->ID;
	}

	/**
	 * Delete database row for specified ID
	 *
	 * @param  integer $id ID of database row
	 *
	 * @return bool True on success, false on failure
	 */
	public static function delete( int $id ) {
		global $wpdb;

		$table = static::$table;

		// Run the query, will return true if deleted, false otherwise.
		$result = (bool) $wpdb->delete( "{$wpdb->prefix}{$table}", [ 'ID' => $id ], [ '%d' ] );

		// Remove cached entry.
		wp_cache_delete( $id, $table );

		return $result;
	}

	/**
	 * Retrieve model instance via ID
	 *
	 * @param  integer $id ID of database row
	 *
	 * @return Model|false Instance of model on success, false on failure
	 */
	public static function find( $id ) {
		global $wpdb;

		$table = static::$table;

		$data = wp_cache_get( $id, $table );

		if ( ! $data ) {
			$data = $wpdb->get_row(
				$wpdb->prepare(
					'SELECT * FROM %i WHERE ID = %d LIMIT 1;',
					$wpdb->prefix . $table,
					$id
				),
				ARRAY_A
			);

			if ( ! $data ) {
				return false;
			}

			wp_cache_add( $id, $data, $table );
		}

		// Create instance of current class and return.
		return new static( $data );
	}
}
