<?php

namespace SilverStripe\ORM;

use Generator;

/**
 * Library of static methods for manipulating arrays.
 */
class ArrayLib
{

    /**
     * Inverses the first and second level keys of an associative
     * array, keying the result by the second level, and combines
     * all first level entries within them.
     *
     * Before:
     * <example>
     * array(
     *    'row1' => array(
     *        'col1' =>'val1',
     *        'col2' => 'val2'
     *    ),
     *    'row2' => array(
     *        'col1' => 'val3',
     *        'col2' => 'val4'
     *    )
     * )
     * </example>
     *
     * After:
     * <example>
     * array(
     *    'col1' => array(
     *        'row1' => 'val1',
     *        'row2' => 'val3',
     *    ),
     *    'col2' => array(
     *        'row1' => 'val2',
     *        'row2' => 'val4',
     *    ),
     * )
     * </example>
     *
     * @param array $arr
     * @return array
     */
    public static function invert($arr)
    {
        if (!$arr) {
            return [];
        }

        $result = array();

        foreach ($arr as $columnName => $column) {
            foreach ($column as $rowName => $cell) {
                $result[$rowName][$columnName] = $cell;
            }
        }

        return $result;
    }

    /**
     * Return an array where the keys are all equal to the values.
     *
     * @param $arr array
     * @return array
     */
    public static function valuekey($arr)
    {
        return array_combine($arr, $arr);
    }

    /**
     * @todo Improve documentation
     *
     * @param array $arr
     * @return array
     */
    public static function array_values_recursive($arr)
    {
        $lst = array();

        foreach (array_keys($arr) as $k) {
            $v = $arr[$k];
            if (is_scalar($v)) {
                $lst[] = $v;
            } elseif (is_array($v)) {
                $lst = array_merge(
                    $lst,
                    self::array_values_recursive($v)
                );
            }
        }

        return $lst;
    }

    /**
     * Filter an array by keys (useful for only allowing certain form-input to
     * be saved).
     *
     * @param $arr array
     * @param $keys array
     *
     * @return array
     */
    public static function filter_keys($arr, $keys)
    {
        foreach ($arr as $key => $v) {
            if (!in_array($key, $keys)) {
                unset($arr[$key]);
            }
        }

        return $arr;
    }

    /**
     * Determines if an array is associative by checking for existing keys via
     * array_key_exists().
     *
     * @see http://nz.php.net/manual/en/function.is-array.php#76188
     *
     * @param array $arr
     *
     * @return boolean
     */
    public static function is_associative($arr)
    {
        if (is_array($arr) && !empty($arr)) {
            for ($iterator = count($arr) - 1; $iterator; $iterator--) {
                if (!array_key_exists($iterator, $arr)) {
                    return true;
                }
            }

            return !array_key_exists(0, $arr);
        }

        return false;
    }

    /**
     * Recursively searches an array $haystack for the value(s) $needle.
     *
     * Assumes that all values in $needle (if $needle is an array) are at
     * the SAME level, not spread across multiple dimensions of the $haystack.
     *
     * @param mixed $needle
     * @param array $haystack
     * @param boolean $strict
     *
     * @return boolean
     */
    public static function in_array_recursive($needle, $haystack, $strict = false)
    {
        if (!is_array($haystack)) {
            return false;
        }

        if (in_array($needle, $haystack, $strict)) {
            return true;
        } else {
            foreach ($haystack as $obj) {
                if (self::in_array_recursive($needle, $obj, $strict)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Similar to array_map, but recurses when arrays are encountered.
     *
     * Actually only one array argument is supported.
     *
     * @param $f callback to apply
     * @param $array array
     * @return array
     */
    public static function array_map_recursive($f, $array)
    {
        $applyOrRecurse = function ($v) use ($f) {
            return is_array($v) ? ArrayLib::array_map_recursive($f, $v) : call_user_func($f, $v);
        };

        return array_map($applyOrRecurse, $array);
    }

    /**
     * Recursively merges two or more arrays.
     *
     * Behaves similar to array_merge_recursive(), however it only merges
     * values when both are arrays rather than creating a new array with
     * both values, as the PHP version does. The same behaviour also occurs
     * with numeric keys, to match that of what PHP does to generate $_REQUEST.
     *
     * @param array $array
     *
     * @return array
     */
    public static function array_merge_recursive($array)
    {
        $arrays = func_get_args();
        $merged = array();

        if (count($arrays) == 1) {
            return $array;
        }

        while ($arrays) {
            $array = array_shift($arrays);

            if (!is_array($array)) {
                trigger_error(
                    'SilverStripe\ORM\ArrayLib::array_merge_recursive() encountered a non array argument',
                    E_USER_WARNING
                );
                return [];
            }

            if (!$array) {
                continue;
            }

            foreach ($array as $key => $value) {
                if (is_array($value) && array_key_exists($key, $merged) && is_array($merged[$key])) {
                    $merged[$key] = ArrayLib::array_merge_recursive($merged[$key], $value);
                } else {
                    $merged[$key] = $value;
                }
            }
        }

        return $merged;
    }

    /**
     * Takes an multi dimension array and returns the flattened version.
     *
     * @param array $array
     * @param boolean $preserveKeys
     * @param array $out
     * @return array
     */
    public static function flatten($array, $preserveKeys = true, &$out = array())
    {
        foreach ($array as $key => $child) {
            if (is_array($child)) {
                $out = self::flatten($child, $preserveKeys, $out);
            } else {
                if ($preserveKeys) {
                    $out[$key] = $child;
                } else {
                    $out[] = $child;
                }
            }
        }

        return $out;
    }

    /**
     * Iterate list, but allowing for modifications to the underlying list.
     * Items in $list will only be iterated exactly once for each key, and supports
     * items being removed or deleted.
     * List must be associative.
     *
     * @param array $list
     * @return Generator
     */
    public static function iterateVolatile(array &$list)
    {
        // Keyed by already-iterated items
        $iterated = [];
        // Get all items not yet iterated
        while ($items = array_diff_key($list, $iterated)) {
            // Yield all results
            foreach ($items as $key => $value) {
                // Skip items removed by a prior step
                if (array_key_exists($key, $list)) {
                    // Ensure we yield from the source list
                    $iterated[$key] = true;
                    yield $key => $list[$key];
                }
            }
        }
    }
}
