<?php
namespace Jet_Engine\Relations;

use Jet_Engine\Relations\Storage\Ordering;

/**
 * Relation object
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Arguments schema:
 *
 * 'parent_object'  => 'posts::page' - information about parent object in type::subtype format
 * 'child_object'   => 'posts::post' - information about child object in type::subtype format
 * 'parent_rel'     => null - ID of parent relation
 * 'type'           => 'one_to_one' - relation type, allowed values - 'one_to_one', 'one_to_many', 'many_to_many'
 * 'db_table'       => true - register or not separate DB table to store all related items
 * 'parent_control' => true - register or not control for related children on the parent object edit page
 * 'child_control'  => true - register or not control for related parents on the children objects edit page
 * 'parent_manager' => true - allow to create new objects of children type from parent object edit page
 * 'child_manager'  => true - allow to create new objects of parent type from children objects edit page
 * 'parent_allow_delete' => true - allow to delete objects of children type from parent object edit page
 * 'child_allow_delete'  => true - allow to delete objects of parent type from children objects edit page
 * 'parent_table' => array( 'image' => array( 'enable' => true, 'callback' => '', 'name' => 'Image' ) ) - additional table columns for parent object edit page control
 * 'child_table'  => array( 'image' => array( 'enable' => true, 'callback' => '', 'name' => 'Image' ) ) - additional table columns for child object edit page control
 * 'meta_fields'    => array() - list of meta fields for relation
 * 'id'             => $id - relation ID
 *
 */
class Relation {

	protected $raw_args = array();
	protected $rel_id = array();
	protected $controls;
	protected $rel_cache_group = 'jet_engine_rel';
	protected $update_context  = null;
	protected $control_context = null;
	protected $query_order = array();

	/**
	 * @var Storage\DB
	 */
	public $db;
	/**
	 * @var Storage\DB
	 */
	public $meta_db;

	/**
	 * @param integer $rel_id relation ID
	 * @param array   $args   relation arguments
	 * @param boolean $silent is silent activation or not. if silent - only props will be filled, no hooks added
	 */
	public function __construct( $rel_id = 0, $args = array(), $silent = false ) {

		$this->raw_args = apply_filters( 'jet-engine/relations/raw-args', $args, $rel_id );
		$this->rel_id   = $rel_id;

		$this->setup_db();

		if ( $silent ) {
			return;
		}

		if ( is_admin() && $this->is_valid() ) {
			$this->setup_controls();
		}

		add_action( 'rest_api_init', array( $this, 'init_public_rest_api' ) );

		jet_engine()->relations->types_helper->register_cleanup_hook( $this->get_args( 'parent_object' ), array( $this, 'cleanup_relation' ) );
		jet_engine()->relations->types_helper->register_cleanup_hook( $this->get_args( 'child_object' ), array( $this, 'cleanup_relation' ) );

		// Context-related hooks
		add_filter( 'jet-engine/listings/allowed-context-list', array( $this, 'register_context' ) );
		add_filter( 'jet-engine/listings/data/object-by-context/' . $this->get_context_name(), array( $this, 'apply_context' ) );

		do_action( 'jet-engine/relations/init/' . $rel_id, $this );

		if ( 'date' === Ordering::instance()->get_mode() ) {
			$this->query_order = array( array(
				'orderby' => 'created',
				'order'   => 'desc',
			) );
		}
	}

	public function init_public_rest_api() {


		$get  = $this->get_args( 'rest_get_enabled' );
		$edit = $this->get_args( 'rest_post_enabled' );

		if ( $get || $edit ) {

			if ( ! class_exists( '\Jet_Engine\Relations\Rest\Public_Controller' ) ) {
				require jet_engine()->relations->component_path( 'rest-api/public-controller.php' );
			}

			$rest_controller = new Rest\Public_Controller();

			$rest_controller->register_routes( array(
				'rel_id' => $this->rel_id,
				'get'    => $get,
				'edit'   => $edit,
			) );
		}

	}

	/**
	 * Check if this relations is can be correctly registered
	 *
	 * @return boolean [description]
	 */
	public function is_valid() {

		$parent_object = jet_engine()->relations->types_helper->type_parts_by_name( $this->get_args( 'parent_object' ) );
		$parent_type   = jet_engine()->relations->types_helper->get_instances( $parent_object[0] );
		$child_object  = jet_engine()->relations->types_helper->type_parts_by_name( $this->get_args( 'child_object' ) );
		$child_type    = jet_engine()->relations->types_helper->get_instances( $child_object[0] );

		if ( ! $parent_type || ! $child_type ) {
			return false;
		}

		return true;

	}

	/**
	 * Return object type class instance for specific relation side
	 *
	 * @param string $object
	 * @return false|object
	 */
	public function get_object_type_for( $object = 'child_object' ) {

		$object = $this->get_args( $object );

		if ( ! $object ) {
			return false;
		}

		$object_data = jet_engine()->relations->types_helper->type_parts_by_name( $object );

		if ( ! $object_data ) {
			return false;
		}

		return jet_engine()->relations->types_helper->get_instances( $object_data[0] );
	}

	/**
	 * Return object name for specific relation side
	 *
	 * @param string $object
	 * @return string|false
	 */
	public function get_object_name_for( $object = 'child_object' ) {

		$object = $this->get_args( $object );

		if ( ! $object ) {
			return false;
		}

		$object_data = jet_engine()->relations->types_helper->type_parts_by_name( $object );

		if ( ! $object_data ) {
			return false;
		}

		return $object_data[1];
	}

	/**
	 * Register context for current relation into allowed context list
	 *
	 * @return [type] [description]
	 */
	public function register_context( $context ) {
		$context[ $this->get_context_name() ] = __( 'Related Items From ', 'jet-engine' ) . $this->get_relation_name();
		return $context;
	}

	/**
	 * Return object for relation context
	 *
	 * @return [type] [description]
	 */
	public function apply_context() {

		$current_object = jet_engine()->listings->data->get_current_object();

		if ( ! $current_object ) {
			return null;
		}

		$object_type   = jet_engine()->relations->types_helper->get_type_for_object( $current_object );
		$related_items = $this->get_related_items_for_object( null, $object_type );

		if ( empty( $related_items ) ) {
			return new \stdClass();
		}

		if ( $this->get_args( 'parent_object' ) === $object_type ) {
			$from_object_type = $this->get_args( 'child_object' );
		} else {
			$from_object_type = $this->get_args( 'parent_object' );
		}

		if ( 1 === count( $related_items ) ) {
			// If we have single realted item - use it as source for object context
			$object = jet_engine()->relations->sources->get_source_object_by_id( $from_object_type, $related_items[0] );
		} else {

			// If we have multiple results - exact context is undefined, so we'll try to retrieve approppriate object from the current stack
			$object = jet_engine()->relations->sources->get_object_from_stack( $from_object_type );

			if ( ! $object ) {
				// fallback
				$object = jet_engine()->relations->sources->get_source_object_by_id( $from_object_type, $related_items[0] );
			}

		}

		if ( is_array( $object ) ) {

			$tmp_object = new \stdClass();

			foreach ( $object as $key => $value ) {
				$tmp_object->$key = $value;
			}

			$object = $tmp_object;

		}

		return $object;

	}

	/**
	 * Returns context name
	 *
	 * @return [type] [description]
	 */
	public function get_context_name() {
		return 'rel_' . $this->get_id();
	}

	/**
	 * Return rel id
	 *
	 * @return [type] [description]
	 */
	public function get_id() {
		return $this->rel_id;
	}

	/**
	 * Return raw arguments of relation intsance
	 *
	 * @return [type] [description]
	 */
	public function get_args( $key = false, $default = false ) {

		if ( ! $key ) {
			return $this->raw_args;
		}

		return isset( $this->raw_args[ $key ] ) ? $this->raw_args[ $key ] : $default;

	}

	/**
	 * Setup DB manager instance
	 *
	 * @return [type] [description]
	 */
	public function setup_db() {

		$db_table = $this->get_args( 'db_table' );

		if ( $db_table ) {
			$this->db      = jet_engine()->relations->storage->get_db_instance( $this->get_id(), jet_engine()->relations->storage->get_db_schema() );
			$this->meta_db = jet_engine()->relations->storage->get_db_instance( $this->get_id() . '_meta', jet_engine()->relations->storage->get_meta_db_schema() );
		} else {
			$this->db      = jet_engine()->relations->storage->get_default_db();
			$this->meta_db = jet_engine()->relations->storage->get_default_meta_db();
		}

	}

	/**
	 * Check if current relation can have only one child item
	 *
	 * @return boolean [description]
	 */
	public function is_single_child() {

		$type       = $this->get_args( 'type' );
		$type       = explode( '_to_', $type );
		$child_type = isset( $type[1] ) ? $type[1] : 'one';

		return ( 'one' === $child_type );
	}

	/**
	 * Check if current relation can have only one parent item
	 *
	 * @return boolean [description]
	 */
	public function is_single_parent() {

		$type       = $this->get_args( 'type' );
		$type       = explode( '_to_', $type );
		$parent_type = isset( $type[0] ) ? $type[0] : 'one';

		return ( 'one' === $parent_type );
	}

	/**
	 * Setup DB manager instance
	 *
	 * @return [type] [description]
	 */
	public function setup_controls() {

		$parent_object  = $this->get_args( 'parent_object' );
		$child_object   = $this->get_args( 'child_object' );
		$parent_control = $this->get_args( 'parent_control' );
		$child_control  = $this->get_args( 'child_control' );
		$type           = $this->get_args( 'type' );
		$type           = explode( '_to_', $type );
		$parent_type    = isset( $type[0] ) ? $type[0] : 'one';
		$child_type     = isset( $type[1] ) ? $type[1] : 'one';

		if ( $parent_control ) {
			$this->control_context = 'parent_control';
			$this->setup_object_controls( $parent_object, $child_type );
		}

		if ( $child_control ) {
			$this->control_context = 'child_control';
			$this->setup_object_controls( $child_object, $parent_type );
		}

	}

	/**
	 * retyurns list of the fields for the create item control
	 * @return [type] [description]
	 */
	public function get_create_control_fields( $for ) {
		$object = $this->get_args( $for );
		return jet_engine()->relations->types_helper->get_create_control_fields( $object, $this );
	}

	/**
	 * Returns new instance of controls class
	 *
	 * @param  [type] $class       [description]
	 * @param  array  $object_data [description]
	 * @param  string $type        [description]
	 * @param  string $label       [description]
	 * @return [type]              [description]
	 */
	public function init_controls_class( $class, $object_data = array() ) {

		if ( ! class_exists( '\Jet_Engine\Relations\Controls\Base' ) ) {

			require_once jet_engine()->relations->component_path( 'controls/base.php' );
			require_once jet_engine()->relations->component_path( 'controls/post-meta.php' );
			require_once jet_engine()->relations->component_path( 'controls/term-meta.php' );
			require_once jet_engine()->relations->component_path( 'controls/user-meta.php' );

			/**
			 * Allways include custom control classes on this hook (if you don't have autoloader implemented)
			 */
			do_action( 'jet-engine/relation/include-controls-class', $this );

		}

		new $class( array(
			'object_type' => $object_data[0],
			'object_name' => $object_data[1],
			'relation'    => $this,
			'context'     => $this->control_context,
		) );

	}

	/**
	 * Setup object controls
	 *
	 * @return [type] [description]
	 */
	public function setup_object_controls( $object, $type ) {

		$object_data = jet_engine()->relations->types_helper->type_parts_by_name( $object );

		if ( ! isset( $object_data[1] ) ) {
			$this->init_controls_class( '\Jet_Engine\Relations\Controls\Post_Meta', array( 'posts', $object ) );
		}

		add_action( 'jet-engine/relation/mix-object-controls/users', array( $this, 'init_users_controls_class' ), 10, 2 );

		switch ( $object_data[0] ) {
			case 'posts':
				$this->init_controls_class( '\Jet_Engine\Relations\Controls\Post_Meta', $object_data );
				break;

			case 'terms':
				$this->init_controls_class( '\Jet_Engine\Relations\Controls\Term_Meta', $object_data );
				break;

			case 'mix':
				do_action( 'jet-engine/relation/mix-object-controls/' . $object_data[1], $object_data, $this );
				break;

			default:
				do_action( 'jet-engine/relation/setup-object-controls/' . $object_data[0], $object_data, $this );
				break;
		}

		remove_action( 'jet-engine/relation/mix-object-controls/users', array( $this, 'init_users_controls_class' ), 10, 2 );

	}

	/**
	 * Initialize users control class
	 * @return [type] [description]
	 */
	public function init_users_controls_class( $object_data ) {
		$this->init_controls_class( '\Jet_Engine\Relations\Controls\User_Meta', $object_data );
	}

	/**
	 * Returns relation metafields
	 *
	 * @return [type] [description]
	 */
	public function get_meta_fields( $format = false, $filter = false, $return = ARRAY_N ) {

		$meta_fields = ! empty( $this->get_args( 'meta_fields' ) ) ? $this->get_args( 'meta_fields' ) : array();

		if ( ! is_array( $meta_fields ) ) {
			$meta_fields = array();
		}

		if ( $format ) {

			if ( ! class_exists( '\Jet_Engine_CPT_Meta' ) ) {
				require_once jet_engine()->plugin_path( 'includes/components/meta-boxes/post.php' );
			}

			$meta_manager = new \Jet_Engine_CPT_Meta( false, $meta_fields );
			$meta_manager->set_blocks_flag();

			$meta_fields = $meta_manager->prepare_meta_fields( $meta_fields );

			foreach ( $meta_fields as $key => $field_data ) {

				$field_data['name'] = $key;

				if ( ! empty( $field_data['description'] ) ) {
					$field_data['description'] = wp_strip_all_tags( $field_data['description'] );
				}

				if ( ! empty( $field_data['name'] ) ) {

					$name_label = sprintf(
						'%s%s',
						__( 'Name: ', 'jet-engine' ),
						$field_data['name']
					);

					if ( ! empty( $field_data['description'] ) ) {
						$field_data['description'] .= ' ' . $name_label;
					} else {
						$field_data['description'] = $name_label;
					}
				}

				if ( ! empty( $field_data['options_callback'] )
					&& is_callable( $field_data['options_callback'] )
				) {

					$field_data['options'] = \Jet_Engine_Tools::get_options_from_callback(
						$field_data['options_callback'], true
					);

					unset( $field_data['options_callback'] );
				}

				$meta_fields[ $key ] = $field_data;

			}

		}

		if ( ARRAY_N === $return ) {
			$meta_fields = array_values( $meta_fields );
		} elseif ( ! $format ) {
			foreach ( $meta_fields as $field_data ) {
				$meta_fields[ $field_data['name'] ] = $field_data;
			}
		}

		if ( $filter && is_callable( $filter ) ) {
			return array_map( $filter, $meta_fields );
		} else {
			return $meta_fields;
		}
	}

	/**
	 * Register new column for current relation
	 * @param [type] $key      [description]
	 * @param [type] $callback [description]
	 */
	public function add_table_column( $object, $key, $name, $callback ) {

		// if we adds columns for parent object of relation, table with items of this object will be shown on child object page,
		// so we need to modify a child table and vice versa
		$arg = ( 'parent_object' === $object ) ? 'child_table' : 'parent_table';

		$current = $this->get_args( $arg, array() );
		$current[ $key ] = array(
			'enabled'  => true,
			'name'     => $name,
			'callback' => $callback,
		);

		$this->raw_args[ $arg ] = $current;

	}

	/**
	 * Returns available columns list fro given object
	 *
	 * @return [type] [description]
	 */
	public function get_table_columns_for_object( $object ) {

		$defaults = array(
			'title' => __( 'Title', 'jet-engine' ),
		);

		$custom_cols = $this->get_object_column( $object );

		if ( ! empty( $custom_cols ) ) {
			foreach ( $custom_cols as $key => $data ) {
				if ( ! empty( $data['enabled'] ) ) {
					$defaults[ $key ] = $data['name'];
				}
			}
		}

		$meta_fields = $this->get_meta_fields();

		if ( ! empty( $meta_fields ) ) {
			$defaults['meta'] = __( 'Meta Data', 'jet-engine' );
		}

		$defaults['actions'] = __( 'Actions', 'jet-engine' );

		return $defaults;

	}

	/**
	 * Check if given object has given columns enabled for edit page table of this object
	 *
	 * @param  [type] $object [description]
	 * @param  [type] $column [description]
	 * @return [type]         [description]
	 */
	public function object_has_column( $object, $column ) {
		$col = $this->get_object_column( $object, $column );
		return ( ! empty( $col['enabled'] ) ) ? true : false;
	}

	/**
	 * Returns information about given columns for given object
	 *
	 * @param  [type] $object [description]
	 * @param  [type] $column [description]
	 * @return [type]         [description]
	 */
	public function get_object_column( $object, $column = null ) {

		$option_key = ( $object === $this->get_args( 'parent_object' ) ) ? 'child_table' : 'parent_table';
		$table_data = $this->get_args( $option_key, array() );
		$col        = array();

		if ( ! $column ) {
			return $table_data;
		}

		if ( ! empty( $table_data ) ) {
			$col = $table_data[ $column ];
		}

		return $col;

	}

	/**
	 * Returns relation name by objects
	 *
	 * @return [type] [description]
	 */
	public function get_relation_name() {

		$labels = $this->get_args( 'labels' );

		// Returns human-readable name if defined
		if ( ! empty( $labels['name'] ) ) {
			return $labels['name'];
		}

		// If not- generate name automatically
		$parent_object = $this->get_args( 'parent_object' );
		$child_object  = $this->get_args( 'child_object' );

		$parent_object = jet_engine()->relations->types_helper->type_parts_by_name( $parent_object );
		$child_object  = jet_engine()->relations->types_helper->type_parts_by_name( $child_object );

		return sprintf(
			__( '%1$s to %2$s' ),
			jet_engine()->relations->types_helper->get_type_label( 'plural', $parent_object[0], $parent_object[1] ),
			jet_engine()->relations->types_helper->get_type_label( 'plural', $child_object[0], $child_object[1] )
		);

	}

	/**
	 * Returns related items for given object
	 * Automatically detects - we need to get children or parent items by object.
	 *
	 * @param  [type] $object [description]
	 * @return [type]         [description]
	 */
	public function get_related_items_for_object( $object = null, $object_type = null ) {

		if ( ! $object_type ) {
			$object_type = jet_engine()->relations->types_helper->get_type_for_object( $object );
		}

		if ( $this->get_args( 'parent_object' ) === $object_type ) {
			return $this->get_children( jet_engine()->listings->data->get_current_object_id( $object ), 'ids' );
		} else {
			return $this->get_parents( jet_engine()->listings->data->get_current_object_id( $object ), 'ids' );
		}

		return array();

	}

	/**
	 * Returns related children id/ids
	 *
	 * @param  [type] $parent_id [description]
	 * @return [type]            [description]
	 */
	public function get_children( $parent_id, $fields = 'all' ) {

		$query_args = array( array(
			'field' => 'rel_id',
			'value' => $this->get_id(),
		) );

		if ( is_array( $parent_id ) ) {
			$query_args[] = array(
				'field'    => 'parent_object_id',
				'value'    => $parent_id,
				'operator' => 'IN',
			);
		} elseif ( $parent_id ) {
			$query_args[] = array(
				'field' => 'parent_object_id',
				'value' => $parent_id,
			);
		}

		$cache_key = $this->get_cache_key( $parent_id, '0' );
		$children  = wp_cache_get( $cache_key, $this->rel_cache_group );

		if ( ! $children ) {
			$children = $this->get_items( $query_args );
			wp_cache_set( $cache_key, $children, $this->rel_cache_group );
		}

		if ( 'all' !== $fields ) {
			$children = array_map( function( $item ) use ( $fields ) {
				return $item['child_object_id'];
			}, $children );
		}

		$result = ! empty( $children ) ? $children : array();

		return apply_filters( 'jet-engine/relations/get-children', $result, $parent_id, $fields, $this );

	}

	/**
	 * Returns related parent id/ids
	 *
	 * @param  [type] $parent_id [description]
	 * @return [type]            [description]
	 */
	public function get_parents( $child_id, $fields = 'all' ) {

		$query_args = array( array(
			'field' => 'rel_id',
			'value' => $this->get_id(),
		) );

		if ( is_array( $child_id ) ) {
			$query_args[] = array(
				'field'    => 'child_object_id',
				'value'    => $child_id,
				'operator' => 'IN',
			);
		} elseif ( $child_id ) {
			$query_args[] = array(
				'field' => 'child_object_id',
				'value' => $child_id,
			);
		}

		$cache_key = $this->get_cache_key( '0', $child_id );
		$parents   = wp_cache_get( $cache_key, $this->rel_cache_group );

		if ( ! $parents ) {
			$parents = $this->get_items( $query_args );
			wp_cache_set( $cache_key, $parents, $this->rel_cache_group );
		}

		if ( 'all' !== $fields ) {
			$parents = array_map( function( $item ) use ( $fields ) {
				return $item['parent_object_id'];
			}, $parents );
		}

		$result = ! empty( $parents ) ? $parents : array();

		return apply_filters( 'jet-engine/relations/get-parents', $result, $child_id, $fields, $this );

	}

	/**
	 * Returns related siblings list
	 *
	 * @param  [type] $object_id [description]
	 * @param  [type] $from      [description]
	 * @param  string $fields    [description]
	 * @return [type]            [description]
	 */
	public function get_siblings( $object_id = null, $from = 'child_object', $fields = 'all' ) {

		if ( ! $object_id ) {
			return array();
		}

		$query_args = array( array(
			'field' => 'rel_id',
			'value' => $this->get_id(),
		) );

		$from_object = $from . '_id';
		$rel_object  = ( 'parent_object_id' === $from_object ) ? 'child_object_id' : 'parent_object_id';

		if ( 'parent_object_id' === $rel_object ) {
			$rel_ids = $this->get_parents( $object_id, 'ids' );
		} else {
			$rel_ids = $this->get_children( $object_id, 'ids' );
		}

		if ( empty( $rel_ids ) ) {
			return array();
		}

		$query_args[] = array(
			'field'    => $rel_object,
			'value'    => $rel_ids,
			'operator' => 'IN',
		);

		if ( is_array( $object_id ) ) {
			$query_args[] = array(
				'field'    => $from_object,
				'value'    => $object_id,
				'operator' => 'NOT IN',
			);
		} elseif ( $object_id ) {
			$query_args[] = array(
				'field'    => $from_object,
				'value'    => $object_id,
				'operator' => '!=',
			);
		}

		$cache_key = $this->get_cache_key( $from_object, $object_id, array( 'siblings' ) );
		$result    = wp_cache_get( $cache_key, $this->rel_cache_group );

		if ( ! $result ) {
			$result = $this->get_items( $query_args );
		}

		if ( 'all' !== $fields ) {
			$result = array_map( function( $item ) use ( $fields, $from_object ) {
				return $item[ $from_object ];
			}, $result );
		}

		$result = ! empty( $result ) ? $result : array();

		return apply_filters( 'jet-engine/relations/get-siblings', $result, $object_id, $fields, $this );

	}

	/**
	 * Internal wrapper to db::query method
	 *
	 * @param  array  $query_args [description]
	 * @return [type]             [description]
	 */
	public function get_items( $query_args = [] ) {
		return $this->db->query( $query_args, 0, 0, $this->query_order );
	}

	/**
	 * Check if given object type and name combination is arent fro current relation
	 *
	 * @return boolean [description]
	 */
	public function is_parent( $type, $name ) {
		$parent_object = $this->get_args( 'parent_object' );
		return $parent_object === jet_engine()->relations->types_helper->type_name_by_parts( $type, $name );
	}

	/**
	 * Callback to remove related items on deletion of initial item from given object
	 *
	 * @param  [type] $object  [description]
	 * @param  [type] $item_id [description]
	 * @return [type]          [description]
	 */
	public function cleanup_relation( $object, $item_id ) {

		$parent_object = false;
		$child_object  = false;

		if ( $this->get_args( 'parent_object' ) === $object ) {
			$parent_object = $item_id;
		} else {
			$child_object = $item_id;
		}

		$this->delete_rows( $parent_object, $child_object, true );
		$this->reset_db_cache();

	}

	/**
	 * Delete parent - child pair for current relation.
	 *
	 * if $parent_object is empty - will be deleted all rows for current relations which has $child_object
	 * if $child_object is empty - will be deleted all rows for current relations which has $parent_object
	 * if both empty - will be deleted all rows for current relation
	 *
	 * @param  [type]  $parent_object [description]
	 * @param  [type]  $child_object  [description]
	 * @return [type]                 [description]
	 */
	public function delete_rows( $parent_object = false, $child_object = false, $clear_meta = true ) {

		$delete_where = array(
			'rel_id' => $this->get_id(),
		);

		if ( false !== $parent_object ) {
			$delete_where['parent_object_id'] = $parent_object;
		}

		if ( false !== $child_object ) {
			$delete_where['child_object_id'] = $child_object;
		}

		if ( $clear_meta && $this->meta_db->is_table_exists() ) {
			$this->meta_db->delete( $delete_where );
		}

		$this->db->delete( $delete_where );

		do_action( 'jet-engine/relation/delete/after', $parent_object, $child_object, $clear_meta, $this );

		$this->flush_cache( $parent_object, $child_object );

		$this->reset_db_cache();

	}

	/**
	 * Clean up meta fields which are exists in the DB
	 * @param  boolean $meta_fields [description]
	 * @return [type]               [description]
	 */
	public function cleanup_meta( $parent_object = null, $child_object = null, $meta_fields = false ) {

		if ( ! $meta_fields ) {
			$meta_fields = $this->get_meta_fields();
		}

		$allowed_keys = array();

		foreach ( $meta_fields as $field ) {
			$allowed_keys[] = $field['name'];
		}

		$all_meta = $this->get_all_meta( $parent_object, $child_object );

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

		if ( $child_object ) {
			foreach ( $all_meta as $meta_key => $meta_value ) {
				if ( ! in_array( $meta_key, $allowed_keys ) ) {
					$this->delete_meta( $parent_object, $child_object, $meta_key );
				}
			}
		} else {
			foreach ( $all_meta as $child_id => $child_meta ) {
				foreach ( $child_meta as $meta_key => $meta_value ) {
					if ( ! in_array( $meta_key, $allowed_keys ) ) {
						$this->delete_meta( $parent_object, $child_id, $meta_key );
					}
				}
			}
		}

		$this->reset_db_cache();

	}

	/**
	 * Update relation data in the DB
	 *
	 * @param  [type]  $parent_object [description]
	 * @param  [type]  $child_object  [description]
	 * @return [type]                 [description]
	 */
	public function update_all_meta( $new_meta = array(), $parent_object = null, $child_object = null ) {

		if ( null === $parent_object || null === $child_object ) {
			return;
		}

		if ( ! $this->meta_db->is_table_exists() ) {
			$this->meta_db->create_table();
		}

		$allowed_meta = $this->get_meta_fields();

		$this->cleanup_meta( $parent_object, $child_object, $allowed_meta );

		foreach ( $allowed_meta as $field ) {

			$name = ! empty( $field['name'] ) ? $field['name'] : false;

			if ( ! $name ) {
				continue;
			}

			if ( isset( $new_meta[ $name ] ) ) {

				$value = $this->sanitize_meta( $new_meta[ $name ], $field );
				$this->update_meta( $parent_object, $child_object, $name, $value );

			} else {
				$this->delete_meta( $parent_object, $child_object, $name );
			}

		}

		do_action( 'jet-engine/relation/update-all-meta/after', $parent_object, $child_object, $new_meta, $this );

		$this->flush_cache( $parent_object, $child_object );
		$this->reset_db_cache();

	}

	/**
	 * Get formatted meta data output for editor
	 *
	 * @param  array  $meta [description]
	 * @return [type]       [description]
	 */
	public function format_meta( $meta = array() ) {

		if ( empty( $meta ) ) {
			return array();
		}

		$fields = $this->get_meta_fields( false, false, ARRAY_A );

		$with_options = $this->get_meta_fields(
			true,
			function( $field ) {
				return is_array( $field['options'] ?? false ) ? $field['options'] : array();
			},
			ARRAY_A
		);

		foreach ( $meta as $key => $value ) {

			$field = isset( $fields[ $key ] ) ? $fields[ $key ] : false;

			if ( ! $field ) {
				continue;
			}

			switch ( $field['type'] ) {

				case 'date':

					if ( ! empty( $field['is_timestamp'] ) && \Jet_Engine_Tools::is_valid_timestamp( $value ) ) {
						$value = date( 'Y-m-d', $value );
					}

					break;

				case 'datetime-local':

					if ( ! empty( $field['is_timestamp'] ) && \Jet_Engine_Tools::is_valid_timestamp( $value ) ) {
						$value = date( 'Y-m-d\TH:i', $value );
					}

					break;
				case 'checkbox':
				case 'select':

					if ( $field['type'] === 'select' && empty( $field['is_multiple'] ) ) {
						break;
					}

					$option_values = array_column( $with_options[ $field[ 'name' ] ], 'value' );

					$value = array_intersect( $value, $option_values );
					$value = array_unique( $value );
					$value = array_values( $value );

					break;

			}

			$meta[ $key ] = $value;

		}

		return $meta;

	}

	/**
	 * Sanitize meta field by field data
	 *
	 * @param  [type] $input [description]
	 * @param  [type] $field [description]
	 * @return [type]        [description]
	 */
	public function sanitize_meta( $input, $field ) {

		switch ( $field['type'] ) {

			case 'date':
			case 'datetime-local':

				if ( ! empty( $field['is_timestamp'] ) && ! \Jet_Engine_Tools::is_valid_timestamp( $input ) ) {
					$input = strtotime( $input );
				}

				break;
			case 'checkbox':
			case 'select':

				if ( $field['type'] === 'select' && empty( $field['is_multiple'] ) ) {
					break;
				}

				$with_options = $this->get_meta_fields(
					true,
					function( $field ) {
						return is_array( $field['options'] ?? false ) ? $field['options'] : array();
					},
					ARRAY_A
				);

				$option_values = array_column( $with_options[ $field[ 'name' ] ], 'value' );

				$input = array_intersect( $input, $option_values );
				$input = array_unique( $input );
				$input = array_values( $input );

				break;

		}

		return $input;

	}

	/**
	 * Update meta for parent+child pair
	 *
	 * @param  [type] $parent_object [description]
	 * @param  [type] $child_object  [description]
	 * @param  [type] $meta_key      [description]
	 * @param  string $meta_value    [description]
	 * @return [type]                [description]
	 */
	public function update_meta( $parent_object, $child_object, $meta_key, $meta_value = '' ) {

		$query = array(
			'rel_id'           => $this->get_id(),
			'parent_object_id' => $parent_object,
			'child_object_id'  => $child_object,
			'meta_key'         => $meta_key,
		);

		$exists = $this->meta_db->query( $query );

		if ( ! empty( $exists ) ) {
			$this->meta_db->update( array( 'meta_value' => $meta_value ), $query );
		} else {
			$query['meta_value'] = $meta_value;
			$this->meta_db->insert( $query );
		}

		$this->flush_cache( $parent_object, $child_object );

		$this->reset_db_cache();

	}

	/**
	 * Delete meta row
	 *
	 * @param  [type] $parent_object [description]
	 * @param  [type] $child_object  [description]
	 * @param  [type] $meta_key      [description]
	 * @return [type]                [description]
	 */
	public function delete_meta( $parent_object, $child_object = '', $meta_key = null ) {

		$query = array(
			'rel_id'           => $this->get_id(),
			'parent_object_id' => $parent_object,
		);

		if ( $child_object ) {
			$query['child_object_id'] = $child_object;
		}

		if ( $meta_key ) {
			$query['meta_key'] = $meta_key;
		}

		$this->meta_db->delete( array(
			'rel_id'           => $this->get_id(),
			'parent_object_id' => $parent_object,
			'child_object_id'  => $child_object,
			'meta_key'         => $meta_key,
		) );

		$this->flush_cache( $parent_object, $child_object );

		$this->reset_db_cache();

	}

	/**
	 * Get meta value by key for parent+child pair
	 *
	 * @param  [type] $parent_object [description]
	 * @param  [type] $child_object  [description]
	 * @param  [type] $meta_key      [description]
	 * @return [type]                [description]
	 */
	public function get_meta( $parent_object, $child_object, $meta_key ) {
		$meta = $this->get_all_meta( $parent_object, $child_object );
		return isset( $meta[ $meta_key ] ) ? $meta[ $meta_key ] : false;
	}

	/**
	 * Check if relation meta field stores array values.
	 *
	 * @param  string $meta_key Meta key.
	 * @return boolean
	 */
	protected function is_array_meta_field( $meta_key ) {

		$meta_fields = $this->get_meta_fields( false, false, ARRAY_A );
		$field       = isset( $meta_fields[ $meta_key ] ) ? $meta_fields[ $meta_key ] : false;

		if ( ! $field || empty( $field['type'] ) ) {
			return false;
		}

		switch ( $field['type'] ) {
			case 'checkbox':
				return true;

			case 'select':
				return ! empty( $field['is_multiple'] );
		}

		return (bool) apply_filters(
			'jet-engine/relations/meta/is-array-field',
			false,
			$field,
			$meta_key,
			$this
		);
	}

	/**
	 * Normalize raw relation meta value based on field schema.
	 *
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $value    Raw value.
	 * @return mixed
	 */
	protected function normalize_meta_value( $meta_key, $value ) {

		if ( ! is_string( $value ) ) {
			return $value;
		}

		if ( ! $this->is_array_meta_field( $meta_key ) ) {
			return $value;
		}

		$unserialized = jet_engine_safe_unserialize( $value, 'relation_meta_storage' );

		return is_array( $unserialized ) ? $unserialized : $value;
	}

	/**
	 * Get all existing meta for parent+child pair
	 *
	 * @param  [type] $parent_object [description]
	 * @param  [type] $child_object  [description]
	 * @return [type]                [description]
	 */
	public function get_all_meta( $parent_object, $child_object = '' ) {

		$cache_key = $this->get_cache_key( $parent_object, $child_object );
		$meta      = wp_cache_get( $cache_key, $this->rel_cache_group );

		if ( false === $meta ) {

			$query = array(
				'rel_id'           => $this->get_id(),
				'parent_object_id' => $parent_object,
			);

			if ( $child_object ) {
				$query['child_object_id'] = $child_object;
			}

			$meta = $this->meta_db->query( $query );

			if ( ! empty( $meta ) ) {

				$prepared_meta = array();

				foreach ( $meta as $meta_row ) {
					$meta_value = $this->normalize_meta_value( $meta_row['meta_key'], $meta_row['meta_value'] );

					if ( $child_object ) {
						$prepared_meta[ $meta_row['meta_key'] ] = $meta_value;
					} else {

						$child_id = $meta_row['child_object_id'];

						if ( ! isset( $prepared_meta[ $child_id ] ) ) {
							$prepared_meta[ $child_id ] = array();
						}

						$prepared_meta[ $child_id ][ $meta_row['meta_key'] ] = $meta_value;

					}
				}

				$meta = $prepared_meta;

			}

			wp_cache_set( $cache_key, $meta, $this->rel_cache_group );

		}

		return $meta;

	}

	/**
	 * Returns meta for current object
	 *
	 * @param  [type] $key [description]
	 * @return [type]      [description]
	 */
	public function get_current_meta( $key ) {

		$current_object_id = jet_engine()->listings->data->get_current_object_id();
		$current_object    = jet_engine()->listings->data->get_current_object();

		$key_data = explode( '::', $key );
		$key = $key_data[0];
		$suffix = isset( $key_data[1] ) ? $key_data[1] : false;



		if ( ! $current_object || ! $current_object_id ) {
			return false;
		}

		$res       = false;
		$parent_id = false;
		$child_id  = false;

		if ( $suffix ) {
			switch ( $suffix ) {
				case 'child':

					$child_id      = $current_object_id;
					$parent_object = jet_engine()->listings->objects_stack->get_parent_object_from_stack();

					if ( $parent_object ) {
						$parent_id  = jet_engine()->listings->data->get_current_object_id( $parent_object );
					}

					break;

				case 'parent':

					$parent_id      = $current_object_id;
					$child_object = jet_engine()->listings->objects_stack->get_parent_object_from_stack();

					if ( $child_object ) {
						$child_id  = jet_engine()->listings->data->get_current_object_id( $child_object );
					}

					break;
			}
		} else {

			$from_object = jet_engine()->relations->types_helper->get_type_for_object( $current_object );

			if ( $this->get_args( 'parent_object' ) === $from_object ) {

				$child_object = jet_engine()->relations->sources->get_object_from_stack( $this->get_args( 'child_object' ) );

				if ( ! $child_object ) {
					return false;
				}

				$parent_id = $current_object_id;
				$child_id  = jet_engine()->listings->data->get_current_object_id( $child_object );

			} else {

				$parent_object = jet_engine()->relations->sources->get_object_from_stack( $this->get_args( 'parent_object' ) );

				if ( ! $parent_object ) {
					return false;
				}

				$parent_id = jet_engine()->listings->data->get_current_object_id( $parent_object );
				$child_id  = $current_object_id;

			}

		}

		if ( $parent_id && $child_id ) {
			$res = $this->get_meta( $parent_id, $child_id, $key );
		}

		return $res;

	}

	/**
	 * Return WP cahce key for current relation parent+child pair
	 *
	 * @param  [type] $parent_object [description]
	 * @param  [type] $child_object  [description]
	 * @return [type]                [description]
	 */
	public function get_cache_key( $parent_object, $child_object, $custom_args = array() ) {

		if ( is_array( $parent_object ) ) {
			$parent_object = implode( '.', $parent_object );
		}

		if ( is_array( $child_object ) ) {
			$child_object = implode( '.', $child_object );
		}

		$custom = '';

		foreach ( $custom_args as $key => $value ) {
			$custom .= $key . '.' . $value;
		}

		return md5( $this->get_id() . $parent_object . $child_object . $custom );
	}

	/**
	 * Update relation data in the DB
	 *
	 * @param  [type]  $parent_object [description]
	 * @param  [type]  $child_object  [description]
	 * @return [type]                 [description]
	 */
	public function update( $parent_object, $child_object ) {

		// todo - update related meta

		if ( ! $this->db->is_table_exists() ) {
			$this->db->create_table();
		}

		$exists = $this->db->query( array(
			'rel_id'           => $this->get_id(),
			'parent_object_id' => $parent_object,
			'child_object_id'  => $child_object,
		) );

		if ( ! empty( $exists ) ) {
			return $exists[0];
		}

		$allow_update = apply_filters( 'jet-engine/relation/update/allow_update', true, $parent_object, $child_object, $this );

		if ( ! $allow_update ) {
			return false;
		}

		do_action( 'jet-engine/relation/update/before', $parent_object, $child_object, $this );

		$update = false;
		$where  = array(
			'rel_id' => $this->get_id()
		);

		if ( $this->is_single_child() ) {

			$children = $this->get_children( $parent_object );

			if ( ! empty( $children ) ) {
				$update                    = true;
				$where['parent_object_id'] = $children[0]['parent_object_id'];
			}

		}

		$context = $this->get_update_context();

		if ( $this->is_single_parent() ) {

			if ( 'parent' === $context ) {
				/**
				 * If we updating parent items from children object,
				 * we need to update exiting parent item fro current child object
				 */
				$parents = $this->get_parents( $child_object );

				if ( ! empty( $parents ) ) {
					$update                   = true;
					$where['child_object_id'] = $parents[0]['child_object_id'];
				}

			} else {
				/**
				 * If we updating children items from parent object,
				 * we need to delete all other parent objects fromm updated child and than insert a new one
				 */
				$this->delete_rows( false, $child_object );
			}

		}

		if ( $update ) {

			$to_update = array(
				'rel_id'           => $this->get_id(),
				'parent_rel'       => $this->get_args( 'parent_rel' ),
				'parent_object_id' => $parent_object,
				'child_object_id'  => $child_object,
			);

			$updated = $this->db->update( $to_update, $where );

			if ( $updated ) {
				$item_id = $this->db->query( $to_update );

				if ( ! empty( $item_id ) ) {
					$item_id = $item_id[0];
				}

			}

		} else {

			$item_id = $this->db->insert( array(
				'rel_id'           => $this->get_id(),
				'parent_rel'       => $this->get_args( 'parent_rel' ),
				'parent_object_id' => $parent_object,
				'child_object_id'  => $child_object,
			) );

		}

		do_action( 'jet-engine/relation/update/after', $parent_object, $child_object, $item_id, $this );

		$this->reset_db_cache();
		$this->reset_update_context();

		$this->flush_cache( $parent_object, $child_object );

		if ( ! empty( $item_id ) && is_array( $item_id ) ) {
			return $item_id;
		} elseif ( $item_id ) {
			return $this->db->get_item( $item_id );
		} else {
			return false;
		}

	}

	public function flush_cache( $parent_object, $child_object ) {
		$cache_key   = $this->get_cache_key( $parent_object, $child_object );
		$p_cache_key = $this->get_cache_key( '0', $child_object );
		$c_cache_key = $this->get_cache_key( $parent_object, '0' );

		wp_cache_delete( $cache_key, $this->rel_cache_group );
		wp_cache_delete( $p_cache_key, $this->rel_cache_group );
		wp_cache_delete( $c_cache_key, $this->rel_cache_group );
	}

	public function reset_db_cache() {
		$this->db->reset_cache();
		$this->meta_db->reset_cache();
	}

	/**
	 * Reset current update context.
	 * Should be called after each relation update to avoid contexts overlaping
	 * @return [type] [description]
	 */
	public function reset_update_context() {
		$this->set_update_context( null );
	}

	/**
	 * Set current update context.
	 * Should be called before relation update to specify in what context we updating it - add parents from child or vice versa
	 * parent - means we seeting up parent related items for the child object (update initiated from child object)
	 * child  - means we seeting up children related items for the parent object (update initiated from parent object)
	 *
	 * @return [type] [description]
	 */
	public function set_update_context( $context ) {
		$this->update_context = $context;
	}

	/**
	 * Get current update context
	 *
	 * @return [type] [description]
	 */
	public function get_update_context() {
		return $this->update_context;
	}

}
