<?php

namespace WP_Ultimo\Dependencies\Arrch;

/**
 * Arrch
 * 
 * @copyright  2012 - 2013
 * @author     Michael Giuliana
 * @link       http://rpnzl.com
 * @version    1.1.0
 * @since      2013/5
 * @license    MIT License - http://opensource.org/licenses/MIT
 * 
 * A small library of array search and sort methods. Perhaps most
 * useful paired with a simple, flat-file storage system, Arrch
 * allows psuedo-queries of large arrays.
 * 
 */
class Arrch
{
    /**
     * @var  arr  $defaults  The find() method default values.
     */
    public static $defaults = array('where' => array(), 'limit' => 0, 'offset' => 0, 'sort_key' => null, 'sort_order' => 'ASC');
    /**
     * @var  str  $key_split  The string to split keys when checking a deep multidimensional array value.
     */
    public static $key_split = '.';
    /**
     * @var  arr  $operators  A list of valid operators for comparisons.
     */
    public static $operators = array('==', '===', '!=', '!==', '>', '<', '>=', '<=', '~', '!~');
    /**
     * This method combines the functionality of the where() and sort() methods,
     * with additional limit and offset parameters. Returns an array of matching
     * array items. Will only sort if a sort key is set.
     * 
     * @param   arr    $data      The array of objects or associative arrays to search.
     * @param   arr    $options   The query options, see Arrch::$defaults.
     * @param   misc   $key       An item's key or index value.
     * @return  arr    The result array.
     */
    public static function find(array $data, array $options = array(), $key = 'all')
    {
        $options = static::merge(static::$defaults, $options);
        $data = static::where($data, $options['where']);
        $data = $options['sort_key'] ? static::sort($data, $options['sort_key'], $options['sort_order']) : $data;
        $data = \array_slice($data, $options['offset'], $options['limit'] == 0 ? null : $options['limit'], \true);
        switch ($key) {
            case 'all':
                return $data;
                break;
            case 'first':
                return \array_shift($data);
                break;
            case 'last':
                return \array_pop($data);
                break;
            default:
                return isset($data[$key]) ? $data[$key] : null;
                break;
        }
    }
    /**
     * Sorts an array of objects by the specified key.
     * 
     * @param   arr   $data    The array of objects or associative arrays to sort.
     * @param   str   $key     The object key or key path to use in sort evaluation.
     * @param   str   $order   ASC or DESC.
     * @return  arr   The result array.
     */
    public static function sort(array $data, $key, $order = null)
    {
        \uasort($data, function ($a, $b) use($key) {
            $a_val = Arrch::extractValues($a, $key);
            $b_val = Arrch::extractValues($b, $key);
            // Strings
            if (\is_string($a_val) && \is_string($b_val)) {
                return \strcasecmp($a_val, $b_val);
            } else {
                if ($a_val == $b_val) {
                    return 0;
                } else {
                    return $a_val > $b_val ? 1 : -1;
                }
            }
        });
        if (\strtoupper($order ?: Arrch::$defaults['sort_order']) == 'DESC') {
            $data = \array_reverse($data, \true);
        }
        return $data;
    }
    /**
     * Evaluates an object based on an array of conditions
     * passed into the function, formatted like so...
     * 
     *      array('city', 'arlington');
     *      array('city', '!=', 'arlington');
     * 
     * @param   arr   $data         The array of objects or associative arrays to search.
     * @param   arr   $conditions   The conditions to evaluate against.
     * @return  arr   The result array.
     */
    public static function where(array $data, array $conditions = array())
    {
        foreach ($conditions as $condition) {
            if (!\is_array($condition)) {
                \trigger_error('Condition ' . \var_export($condition, \true) . ' must be an array!', \E_USER_ERROR);
            } else {
                \array_map(function ($item, $key) use(&$data, $condition) {
                    $return = 0;
                    $operator = \array_key_exists(2, $condition) ? $condition[1] : '===';
                    $search_value = \array_key_exists(2, $condition) ? $condition[2] : $condition[1];
                    if (\is_array($condition[0])) {
                        // array of keys
                        if (\is_array($search_value)) {
                            // array of values
                            foreach ($condition[0] as $prop) {
                                $value = Arrch::extractValues($item, $prop);
                                foreach ($search_value as $query_val) {
                                    $return += Arrch::compare(array($value, $operator, $query_val)) ? 1 : 0;
                                }
                            }
                        } else {
                            // single value
                            foreach ($condition[0] as $prop) {
                                $value = Arrch::extractValues($item, $prop);
                                $return += Arrch::compare(array($value, $operator, $search_value)) ? 1 : 0;
                            }
                        }
                    } elseif (\is_array($search_value)) {
                        // single key, array of query values
                        $value = Arrch::extractValues($item, $condition[0]);
                        $c = 0;
                        foreach ($search_value as $query_val) {
                            $c += Arrch::compare(array($value, $operator, $query_val)) ? 1 : 0;
                        }
                        // Negate options that don't pass all negative assertions
                        if (\in_array($operator, array('!==', '!=', '!~')) && $c < \count($search_value)) {
                            $c = 0;
                        }
                        $return += $c;
                    } else {
                        // single key, single value
                        $value = Arrch::extractValues($item, $condition[0]);
                        $return = Arrch::compare(array($value, $operator, $search_value));
                    }
                    // Unset
                    if (!$return) {
                        unset($data[$key]);
                    }
                }, $data, \array_keys($data));
            }
        }
        return $data;
    }
    /**
     * Finds the requested value in a multidimensional array or an object and
     * returns it.
     * 
     * @param    mixed   $item   The item containing the value.
     * @param    str     $key    The key or key path to the desired value.
     * @return   mixed   The found value or null.
     */
    public static function extractValues($item, $key)
    {
        // Array of results
        $results = array();
        // Cast as array if object
        $item = \is_object($item) ? (array) $item : $item;
        // Array of keys
        $keys = \strstr($key, static::$key_split) ? \explode(static::$key_split, $key) : array($key);
        $keys = \array_filter($keys);
        // Key count
        $key_count = \count($keys);
        // key count > 0
        if ($key_count > 0) {
            $curr_key = \array_shift($keys);
            $arrayValues = \array_values($item);
            $child = \is_array($item) ? \array_shift($arrayValues) : null;
            // it's an array
            if (\is_array($item)) {
                // Clean non-ASCII keys
                foreach ($item as $k => $v) {
                    $sanitized = \preg_replace('/[^(\\x20-\\x7F)]*/', '', $k);
                    if ($sanitized !== $k) {
                        $item[$sanitized] = $v;
                        unset($item[$k]);
                    }
                }
                // it has the keys
                if (\array_key_exists($curr_key, $item)) {
                    $results = \array_merge($results, static::extractValues($item[$curr_key], \implode(static::$key_split, $keys)));
                    // else it's child has the key
                } elseif (\is_array($child) || \is_object($child)) {
                    if (\array_key_exists($curr_key, $child)) {
                        foreach ($item as $child) {
                            $results = \array_merge($results, static::extractValues($child, $key));
                        }
                    }
                }
            }
            // no key
        } else {
            $results[] = $item;
        }
        return $results;
    }
    /**
     * Runs comparison operations on an array of values formatted like so...
     * 
     *      e.g. array($value, $operator, $value)
     * 
     * Returns true or false depending on the outcome of the requested
     * comparative operation (<, >, =, etc..).
     * 
     * @param   arr   $array  The comparison array.
     * @return  bool  Whether the operation evaluates to true or false.
     */
    public static function compare(array $array)
    {
        $return = 0;
        foreach ($array[0] as $value) {
            if ($array[1] === '~') {
                // Check if variables loosely match right away
                if ($value != $array[2]) {
                    if (\is_string($value) && \is_string($array[2])) {
                        $return += \stristr($value, $array[2]) ? 1 : 0;
                    } elseif (\is_null($value) && \is_null($array[2])) {
                        $return += 1;
                    } elseif (\is_array($value) || \is_object($value)) {
                        $return += \stristr(\var_export($value, \true), $array[2]) ? 1 : 0;
                    }
                } else {
                    $return += 1;
                }
            } elseif ($array[1] === '!~') {
                if (\is_string($value) && \is_string($array[2])) {
                    $return += !\stristr($value, $array[2]) ? 1 : 0;
                } elseif (\is_null($value) && \is_null($array[2])) {
                    $return += 0;
                } elseif (\is_array($value) || \is_object($value)) {
                    $return += !\stristr(\var_export($value, \true), $array[2]) ? 1 : 0;
                }
            } else {
                if (\is_array($value) || \is_object($value)) {
                    \trigger_error('Cannot compare object of type ' . \strtoupper(\gettype($value)) . ' with the "' . $array[1] . '" operator.', \E_USER_WARNING);
                }
                if (!\in_array($array[1], static::$operators)) {
                    \trigger_error('Operator "' . $array[1] . '" is not valid.', \E_USER_ERROR);
                } else {
                    switch ($array[1]) {
                        case '==':
                            $return += $value == $array[2] ? 1 : 0;
                            break;
                        case '===':
                            $return += $value === $array[2] ? 1 : 0;
                            break;
                        case '!=':
                            $return += $value != $array[2] ? 1 : 0;
                            break;
                        case '!==':
                            $return += $value !== $array[2] ? 1 : 0;
                            break;
                        case '>':
                            $return += $value > $array[2] ? 1 : 0;
                            break;
                        case '<':
                            $return += $value < $array[2] ? 1 : 0;
                            break;
                        case '>=':
                            $return += $value >= $array[2] ? 1 : 0;
                            break;
                        case '<=':
                            $return += $value <= $array[2] ? 1 : 0;
                            break;
                    }
                }
            }
        }
        return $return;
    }
    /**
     * Recursive array merge for options arrays to eliminate duplicates
     * in a smart manner using a variable number of array arguments.
     * 
     * @param   arr  Various options arrays.
     * @return  arr  The merged array.
     */
    public static function merge()
    {
        $args = \func_get_args();
        $where = array();
        $options = array();
        foreach ($args as $o) {
            $options = \array_merge($options, $o);
            if (isset($o['where'])) {
                if (\count($where) === 0) {
                    $where = $o['where'];
                } else {
                    foreach ($o['where'] as $oc) {
                        $matches = 0;
                        foreach ($where as $wc) {
                            if ($oc == $wc) {
                                $matches += 1;
                            }
                        }
                        if ($matches === 0) {
                            $where[] = $oc;
                        }
                    }
                }
            }
        }
        $options['where'] = $where;
        return $options;
    }
}
