<?php

/**
 * Managed collection
 * @package Simple Lightbox
 * @subpackage Base
 * @author Archetyped
 */
class SLB_Base_Collection extends SLB_Base {
	/* Configuration */

	/**
	 * Set object mode
	 * @var string
	 */
	protected $mode = 'object';

	/**
	 * Item type
	 * @var string
	 */
	protected $item_type = null;

	/**
	 * Property to use for item key
	 * Example: A property or method of the item
	 * @var string
	 */
	protected $key_prop = null;

	/**
	 * Should $key_prop be called or retrieved?
	 * Default: Retrieved (FALSE)
	 * @var bool
	 */
	protected $key_call = false;

	/**
	 * Items in collection unique?
	 * Default: FALSE
	 * @var bool
	 */
	protected $unique = false;

	/* Properties */

	/**
	 * Indexed array of items in collection
	 * @var array
	 */
	protected $items = null;

	/**
	 * Item metadata
	 * Indexed by item key
	 * @var array
	 */
	protected $items_meta = array();

	/* Item Management */

	/**
	 * Initialize collections
	 * Calls `init` action if collection has a hook prefix
	 */
	private function init() {
		// Initialize
		if ( is_null( $this->items ) ) {
			$this->items = array();
			if ( ! empty( $this->hook_prefix ) ) {
				$this->util->do_action( 'init', $this );
			}
		}
	}

	/**
	 * Normalize/Validate item(s)
	 * TODO: If no items are specified, then collection is normalized
	 * Single items are wrapped in an array
	 * @param array|object $items Item(s) to validate
	 * @return array Validated items
	 */
	protected function normalize( $items ) {
		if ( ! is_array( $items ) ) {
			$items = array( $items );
		}
		// Validate item type
		if ( ! is_null( $this->item_type ) ) {
			foreach ( $items as $idx => $item ) {
				// Remove invalid items
				if ( ! ( $item instanceof $this->item_type ) ) {
					unset( $items[ $idx ] );
				}
			}
		}
		if ( ! empty( $items ) ) {
			$items = array_values( $items );
		}
		return $items;
	}

	protected function item_valid( $item ) {
		// Validate item type
		return ( empty( $this->item_type ) || ( $item instanceof $this->item_type ) ) ? true : false;
	}

	/**
	 * Validate item key
	 * Checks collection for existence of key as well
	 * @param string|int $key Key to check collection for
	 * @return bool TRUE if key is valid
	 */
	protected function key_valid( $key ) {
		$this->init();
		return ( ( ( is_string( $key ) && ! empty( $key ) ) || is_int( $key ) ) && isset( $this->items[ $key ] ) ) ? true : false;
	}

	/**
	 * Generate key for item (for storing in collection, etc.)
	 * @param mixed $item Item to generate key for
	 * @return string|null Item key (NULL if no key generated)
	 */
	protected function get_key( $item, $check_existing = false ) {
		$ret = null;
		if ( $this->unique || ! ! $check_existing ) {
			// Check for item in collection
			if ( $this->has( $item ) ) {
				$ret = array_search( $item, $this->items, true );
			} elseif ( ! ! $this->key_prop && ( is_object( $item ) || is_array( $item ) ) ) {
				if ( ! ! $this->key_call ) {
					$cb = $this->util->m( $item, $this->key_prop );
					if ( is_callable( $cb ) ) {
						$ret = call_user_func( $cb );
					}
				} elseif ( is_array( $item ) && isset( $item[ $this->key_prop ] ) ) {
					$ret = $item[ $this->key_prop ];
				} elseif ( is_object( $item ) && isset( $item->{$this->key_prop} ) ) {
					$ret = $item->{$this->key_prop};
				}
			}
		}
		return $ret;
	}

	/**
	 * Add item to collection
	 * @param mixed $item Item to add to collection
	 * @param array $meta (optional) Item metadata
	 * @return Current instance
	 */
	public function add( $item, $meta = null ) {
		$this->init();
		// Validate
		if ( $this->item_valid( $item ) ) {
			// Add item to collection
			$key = $this->get_key( $item );
			if ( ! $key ) {
				$this->items[] = $item;
				$key           = key( $this->items );
			} else {
				$this->items[ $key ] = $item;
			}
			// Add metadata
			if ( ! ! $key && is_array( $meta ) ) {
				$this->add_meta( $key, $meta );
			}
		}
		return $this;
	}

	/**
	 * Remove item from collection
	 * @param int|string $item Key of item to remove
	 * @return Current instance
	 */
	public function remove( $item ) {
		if ( $this->key_valid( $item ) ) {
			unset( $this->items[ $item ] );
		}
		return $this;
	}

	/**
	 * Clear collection
	 * @return Current instance
	 */
	public function clear() {
		$this->items = array();
		return $this;
	}

	/**
	 * Checks if item exists in the collection
	 * @param mixed $item Item(s) to check for
	 * @return bool TRUE if item(s) in collection
	 */
	public function has( $items ) {
		// Attempt to locate item
		return false;
	}

	/**
	 * Retrieve item(s) from collection
	 * If no items specified, entire collection returned
	 * @param array $args (optional) Query arguments
	 * @return object|array Specified item(s)
	 */
	public function get( $args = null ) {
		$this->init();
		// Parse args
		$args_default = array(
			'orderby' => null,
			'order'   => 'DESC',
			'include' => array(),
			'exclude' => array(),
		);
		$r            = wp_parse_args( $args, $args_default );

		$items = $this->items;

		/* Sort */
		if ( ! is_null( $r['orderby'] ) ) {
			// Validate
			if ( ! is_array( $r['orderby'] ) ) {
				$r['orderby'] = array( 'item' => $r['orderby'] );
			}
			// Prep
			$metas = ( isset( $r['orderby']['meta'] ) ) ? $this->items_meta : array();
			// Sort
			foreach ( $r['orderby'] as $stype => $sval ) {
				/* Meta sorting */
				if ( 'meta' === $stype ) {
					// Build sorting buckets
					$buckets = array();
					foreach ( $metas as $item => $meta ) {
						if ( ! isset( $meta[ $sval ] ) ) {
							continue;
						}
						// Create bucket
						$idx = $meta[ $sval ];
						if ( ! isset( $buckets[ $idx ] ) ) {
							$buckets[ $idx ] = array();
						}
						// Add item to bucket
						$buckets[ $idx ][] = $item;
					}
					// Sort buckets
					ksort( $buckets, SORT_NUMERIC );
					// Merge buckets
					$pool = array();
					foreach ( $buckets as $bucket ) {
						$pool = array_merge( $pool, $bucket );
					}
					// Fill with items
					$items = array_merge( array_fill_keys( $pool, null ), $items );
				}
			}
			// Clear workers
			unset( $stype, $sval, $buckets, $pool, $item, $metas, $meta, $idx );
		}
		return $items;
	}

	/* Metadata */

	/**
	 * Add metadata for item
	 * @param string|int $item Item key
	 * @param string|array $meta_key Meta key to set (or array of metadata)
	 * @param mixed $meta_value (optional) Metadata value (if key set)
	 * @param bool $reset (optional) Whether to remove existing metadata first (Default: FALSE)
	 * @return object Current instance
	 */
	protected function add_meta( $item, $meta_key, $meta_value = null, $reset = false ) {
		// Validate
		if ( $this->key_valid( $item ) && ( is_array( $meta_key ) || is_string( $meta_key ) ) ) {
			// Prepare metadata
			$meta = ( is_string( $meta_key ) ) ? array( $meta_key => $meta_value ) : $meta_key;
			// Reset existing meta (if necessary)
			if ( is_array( $meta_key ) && func_num_args() > 2 ) {
				$reset = func_get_arg( 2 );
			}
			if ( ! ! $reset ) {
				unset( $this->items_meta[ $item ] );
			}
			// Add metadata
			if ( ! isset( $this->items_meta[ $item ] ) ) {
				$this->items_meta[ $item ] = array();
			}
			$this->items_meta[ $item ] = array_merge( $this->items_meta[ $item ], $meta );
		}
		return $this;
	}

	/**
	 * Remove item metadata
	 * @param string $item Item key
	 * @return object Current instance
	 */
	protected function remove_meta( $item, $meta_key = null ) {
		if ( $this->key_valid( $item ) && isset( $this->items_meta[ $item ] ) ) {
			if ( is_string( $meta_key ) ) {
				// Remove specific meta value
				unset( $this->items_meta[ $item ][ $meta_key ] );
			} else {
				// Remove all metadata
				unset( $this->items_meta[ $item ] );
			}
		}
		return $this;
	}

	/**
	 * Retrieve metadata
	 * @param string $item Item key
	 * @param string $meta_key (optional) Meta key (All metadata retrieved if no key specified)
	 * @return mixed|null Metadata value
	 */
	protected function get_meta( $item, $meta_key = null ) {
		$ret = null;
		if ( $this->key_valid( $item ) && isset( $this->items_meta[ $item ] ) ) {
			if ( is_null( $meta_key ) ) {
				$ret = $this->items_meta[ $item ];
			} elseif ( is_string( $meta_key ) && isset( $this->items_meta[ $item ][ $meta_key ] ) ) {
				$ret = $this->items_meta[ $item ][ $meta_key ];
			}
		}
		return $ret;
	}

	/* Collection */

	/**
	 * Build entire collection of items
	 * Prints output
	 */
	function build( $build_vars = array() ) {
		// Parse vars
		$this->parse_build_vars( $build_vars );
		$this->util->do_action_ref_array( 'build_init', array( $this ) );
		// Pre-build output
		$this->util->do_action_ref_array( 'build_pre', array( $this ) );
		// Build groups
		$this->build_groups();
		// Post-build output
		$this->util->do_action_ref_array( 'build_post', array( $this ) );
	}

	/**
	 * Parses build variables prior to use
	 * @uses this->reset_build_vars() to reset build variables for each request
	 * @param array $build_vars Variables to use for current request
	 */
	function parse_build_vars( $build_vars = array() ) {
		$this->reset_build_vars();
		$this->build_vars = $this->util->apply_filters( 'parse_build_vars', wp_parse_args( $build_vars, $this->build_vars ), $this );
	}

	/**
	 * Reset build variables to defaults
	 * Default Variables
	 * > groups     - array - Names of groups to build
	 * > context    - string - Context of current request
	 * > layout     - string - Name of default layout to use
	 */
	function reset_build_vars() {
		$this->build_vars = wp_parse_args( $this->build_vars, $this->build_vars_default );
	}
}
