<?php

namespace Application\Service\Extension\Helper;

class ArrayHelper
{

    /**
     * (e.g)
     * ```
     *      $arr = [
     *          '0' => [
     *              'age' => 10,
     *              'name' => 'toom'
     *           ],
     *          '1' => [
     *              'age' => 30,
     *              'name' => 'mary'
     *          ],
     *          '2' => [
     *              'age' => 18,
     *              'name' => 'jerry'
     *          ]
     *     ];
     *      ArrayHelper::multisort($arr, 'age', 'SORT_ASC');
     *      // $arr => [
     *              '0' => [
     *                  'age' => 10,
     *                  'name' => 'toom'
     *              ],
     *              '0' => [
     *                  'age' => 18,
     *                  'name' => 'jerry'
     *              ],
     *              '0' => [
     *                  'age' => 30,
     *                  'name' => 'mary'
     *              ],
     *          ];
     * ```
     * 二维数组排序
     * @param array $array 要排序的数组。
     * @param string|\Closure|array $key 要排序的键。这指的是子数组的键名
     * @param int|array $direction “SORT_ASC”或“SORT_DESC”。
     * @param int|array $sortFlag PHP 排序标志。有效值包括
     * `SORT_REGULAR`、`SORT_NUMERIC`、`SORT_STRING`、`SORT_LOCALE_STRING`、`SORT_NATURAL` 和 `SORT_FLAG_CASE`。
     * 请参考【PHP手册】(https://www.php.net/manual/en/function.sort.php)
     * @throws InvalidArgumentException
     */
    public static function multisort(&$array, $key, $direction = SORT_ASC, $sortFlag = SORT_REGULAR)
    {
        $keys = is_array($key) ? $key : [$key];
        if (empty($keys) || empty($array)) {
            return;
        }
        $n = count($keys);
        if (is_scalar($direction)) {
            $direction = array_fill(0, $n, $direction);
        } elseif (count($direction) !== $n) {
            throw new \Exception('The length of $direction parameter must be the same as that of $keys.');
        }
        if (is_scalar($sortFlag)) {
            $sortFlag = array_fill(0, $n, $sortFlag);
        } elseif (count($sortFlag) !== $n) {
            throw new \Exception('The length of $sortFlag parameter must be the same as that of $keys.');
        }
        $args = [];
        foreach ($keys as $i => $k) {
            $flag = $sortFlag[$i];
            $args[] = array_column($array, $k);
            $args[] = $direction[$i];
            $args[] = $flag;
        }

        // This fix is used for cases when main sorting specified by columns has equal values
        // Without it it will lead to Fatal Error: Nesting level too deep - recursive dependency?
        $args[] = range(1, count($array));
        $args[] = SORT_ASC;
        $args[] = SORT_NUMERIC;

        $args[] = &$array;
        call_user_func_array('array_multisort', $args);
    }

    public static function merge($a, $b)
    {
        $args = func_get_args();
        $res = array_shift($args);
        while (!empty($args)) {
            foreach (array_shift($args) as $k => $v) {
                if (is_int($k)) {
                    if (array_key_exists($k, $res)) {
                        $res[] = $v;
                    } else {
                        $res[$k] = $v;
                    }
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
                    $res[$k] = static::merge($res[$k], $v);
                } else {
                    $res[$k] = $v;
                }
            }
        }

        return $res;
    }


    public static function getColumn($array, $name, $keepKeys = true)
    {
        $result = [];
        if ($keepKeys) {
            foreach ($array as $k => $element) {
                $result[$k] = static::getValue($element, $name);
            }
        } else {
            foreach ($array as $element) {
                $result[] = static::getValue($element, $name);
            }
        }

        return $result;
    }

    /**
     * Retrieves the value of an array element or object property with the given key or property name.
     * If the key does not exist in the array, the default value will be returned instead.
     * Not used when getting value from an object.
     *
     * The key may be specified in a dot format to retrieve the value of a sub-array or the property
     * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would
     * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']`
     * or `$array->x` is neither an array nor an object, the default value will be returned.
     * Note that if the array already has an element `x.y.z`, then its value will be returned
     * instead of going through the sub-arrays. So it is better to be done specifying an array of key names
     * like `['x', 'y', 'z']`.
     *
     * Below are some usage examples,
     *
     * ```php
     * // working with array
     * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username');
     * // working with object
     * $username = \yii\helpers\ArrayHelper::getValue($user, 'username');
     * // working with anonymous function
     * $fullName = \yii\helpers\ArrayHelper::getValue($user, function ($user, $defaultValue) {
     *     return $user->firstName . ' ' . $user->lastName;
     * });
     * // using dot format to retrieve the property of embedded object
     * $street = \yii\helpers\ArrayHelper::getValue($users, 'address.street');
     * // using an array of keys to retrieve the value
     * $value = \yii\helpers\ArrayHelper::getValue($versions, ['1.0', 'date']);
     * ```
     *
     * @param array|object $array array or object to extract value from
     * @param string|\Closure|array $key key name of the array element, an array of keys or property name of the object,
     * or an anonymous function returning the value. The anonymous function signature should be:
     * `function($array, $defaultValue)`.
     * The possibility to pass an array of keys is available since version 2.0.4.
     * @param mixed $default the default value to be returned if the specified array key does not exist. Not used when
     * getting value from an object.
     * @return mixed the value of the element if found, default value otherwise
     */
    public static function getValue($array, $key, $default = null)
    {
        if ($key instanceof \Closure) {
            return $key($array, $default);
        }

        if (is_array($key)) {
            $lastKey = array_pop($key);
            foreach ($key as $keyPart) {
                $array = static::getValue($array, $keyPart);
            }
            $key = $lastKey;
        }

        if (is_object($array) && property_exists($array, $key)) {
            return $array->$key;
        }

        if (static::keyExists($key, $array)) {
            return $array[$key];
        }

        if (($pos = strrpos($key, '.')) !== false) {
            $array = static::getValue($array, substr($key, 0, $pos), $default);
            $key = substr($key, $pos + 1);
        }

        if (static::keyExists($key, $array)) {
            return $array[$key];
        }
        if (is_object($array)) {
            // this is expected to fail if the property does not exist, or __get() is not implemented
            // it is not reliably possible to check whether a property is accessible beforehand
            return $array->$key;
        }

        return $default;
    }

    public static function keyExists($key, $array, $caseSensitive = true)
    {
        foreach (array_keys($array) as $k) {
            if (strcasecmp($key, $k) === 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Indexes and/or groups the array according to a specified key.
     * The input should be either multidimensional array or an array of objects.
     *
     * The $key can be either a key name of the sub-array, a property name of object, or an anonymous
     * function that must return the value that will be used as a key.
     *
     * $groups is an array of keys, that will be used to group the input array into one or more sub-arrays based
     * on keys specified.
     *
     * If the `$key` is specified as `null` or a value of an element corresponding to the key is `null` in addition
     * to `$groups` not specified then the element is discarded.
     *
     * For example:
     *
     * ```php
     * $array = [
     *     ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
     *     ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
     *     ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
     * ];
     * $result = ArrayHelper::index($array, 'id');
     * ```
     *
     * The result will be an associative array, where the key is the value of `id` attribute
     *
     * ```php
     * [
     *     '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
     *     '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
     *     // The second element of an original array is overwritten by the last element because of the same id
     * ]
     * ```
     *
     * An anonymous function can be used in the grouping array as well.
     *
     * ```php
     * $result = ArrayHelper::index($array, function ($element) {
     *     return $element['id'];
     * });
     * ```
     *
     * Passing `id` as a third argument will group `$array` by `id`:
     *
     * ```php
     * $result = ArrayHelper::index($array, null, 'id');
     * ```
     *
     * The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level
     * and indexed by `data` on the third level:
     *
     * ```php
     * [
     *     '123' => [
     *         ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
     *     ],
     *     '345' => [ // all elements with this index are present in the result array
     *         ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
     *         ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
     *     ]
     * ]
     * ```
     *
     * The anonymous function can be used in the array of grouping keys as well:
     *
     * ```php
     * $result = ArrayHelper::index($array, 'data', [function ($element) {
     *     return $element['id'];
     * }, 'device']);
     * ```
     *
     * The result will be a multidimensional array grouped by `id` on the first level, by the `device` on the second one
     * and indexed by the `data` on the third level:
     *
     * ```php
     * [
     *     '123' => [
     *         'laptop' => [
     *             'abc' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
     *         ]
     *     ],
     *     '345' => [
     *         'tablet' => [
     *             'def' => ['id' => '345', 'data' => 'def', 'device' => 'tablet']
     *         ],
     *         'smartphone' => [
     *             'hgi' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
     *         ]
     *     ]
     * ]
     * ```
     *
     * @param array $array the array that needs to be indexed or grouped
     * @param string|\Closure|null $key the column name or anonymous function which result will be used to index the array
     * @param string|string[]|\Closure[]|null $groups the array of keys, that will be used to group the input array
     * by one or more keys. If the $key attribute or its value for the particular element is null and $groups is not
     * defined, the array element will be discarded. Otherwise, if $groups is specified, array element will be added
     * to the result array without any key. This parameter is available since version 2.0.8.
     * @return array the indexed and/or grouped array
     */
    public static function index($array, $key, $groups = [])
    {
        $result = [];
        $groups = (array)$groups;

        foreach ($array as $element) {
            $lastArray = &$result;

            foreach ($groups as $group) {
                $value = static::getValue($element, $group);
                if (!array_key_exists($value, $lastArray)) {
                    $lastArray[$value] = [];
                }
                $lastArray = &$lastArray[$value];
            }

            if ($key === null) {
                if (!empty($groups)) {
                    $lastArray[] = $element;
                }
            } else {
                $value = static::getValue($element, $key);
                if ($value !== null) {
                    if (is_float($value)) {
                        $value = StringHelper::floatToString($value);
                    }
                    $lastArray[$value] = $element;
                }
            }
            unset($lastArray);
        }

        return $result;
    }

    public static function diff(array $array1, array $array2)
    {
        $handleArray = function (&$arr) {
            foreach ($arr as &$item) {
                if (is_array($item)) {
                    $item = json_encode($item);
                }
            }

            return $arr;
        };

        return array_diff_assoc($handleArray($array1), $handleArray($array2));
    }


    public static function newdiff(array $old, array $new): array
    {
        $results = [];

        // 增强键匹配规则（支持纯数字和数字复合键）
        $isNumericKey = function($key) {
            return preg_match('/^\d+(-\d+)*$/', (string)$key);
        };

        // 自定义索引数组检测（兼容 PHP 7.4）
        $isList = function (array $arr) {
            if ($arr === []) return true;
            return array_keys($arr) === range(0, count($arr) - 1);
        };

        // 标准化处理值（解决顺序问题）
        $normalizeValue = function ($value) use ($isList) {
            // 处理字符串转数组
            if (is_string($value) && strpos($value, ',') !== false) {
                $value = explode(',', $value);
            }

            // 仅对索引数组排序
            if (is_array($value) && $isList($value)) {
                sort($value);
            }

            return $value;
        };

        // 递归深度比较
        $deepCompare = function ($a, $b) use (&$deepCompare, $normalizeValue) {
            $a = $normalizeValue($a);
            $b = $normalizeValue($b);

           /* // 处理数值类型差异（37 vs 37.0）
            if (is_numeric($a) && is_numeric($b)) {
                return (float)$a === (float)$b;
            }*/

            // 处理空值与0的比较
            // 0是一个有效的非空值，不应该与空值相等
            $isZero = function($value) {
                return $value === 0 || $value === '0' || $value === 0.0;
            };

            // 检查是否为空值（不包括0）
            $isEmpty = function($value) {
                return $value === null || $value === '' || $value === [] || $value === '[]';
            };

            // 如果一个是0（非空），另一个是空值，则不相等
            if ($isZero($a) && $isEmpty($b)) {
                return false;
            }
            if ($isEmpty($a) && $isZero($b)) {
                return false;
            }

            // 处理其他空值情况（两个都是空值，视为相等）
            if ($isEmpty($a) && $isEmpty($b)) {
                return true;
            }

            //主要处理图片情况 有时类型不一致但其实都是空值
          /*  if (empty($a) && in_array($b, [null, '', [],'[]'], true)) return true;
            if (empty($b) && in_array($a, [null, '', [],'[]'], true)) return true;*/

            if (gettype($a) !== gettype($b)) return false;

            if (is_array($a) && is_array($b)) {
                if (count($a) !== count($b)) return false;

                // 重新索引后比较键
                $aKeys = array_keys($a);
                $bKeys = array_keys($b);
                if ($aKeys !== $bKeys) return false;

                foreach ($a as $k => $v) {
                    if (!$deepCompare($v, $b[$k])) {
                        return false;
                    }
                }
                return true;
            }

            return $a === $b;
        };

        // 处理旧数组键
        foreach ($old as $key => $oldVal) {
            if (!$isNumericKey($key)) continue;

            $keyExists = array_key_exists($key, $new);
            $newVal = $keyExists ? $new[$key] : null;

            if (!$keyExists) {
                $results[$key] = ['status' => 'removed', 'old' => $oldVal, 'new' => null];
            } elseif (!$deepCompare($oldVal, $newVal)) {
                $results[$key] = ['status' => 'modified', 'old' => $oldVal, 'new' => $newVal];
            }
        }

        // 检测新增键
        foreach ($new as $key => $newVal) {
            if (!$isNumericKey($key)) continue;
            if (!array_key_exists($key, $old)) {
                $results[$key] = ['status' => 'added', 'old' => null, 'new' => $newVal];
            }
        }

        return $results;
    }

    public static function remove(array $array, string $key)
    {
        if (is_array($array)) {
            foreach ($array as $subKey => $subArray) {
                if (is_array($subArray)) {
                    // 如果子元素是数组，递归调用函数
                    $array[$subKey] = self::remove($subArray, $key);
                } else {
                    // 否则，检查键是否匹配
                    if ($subKey === $key) {
                        unset($array[$subKey]);
                    }
                }
            }
        }
        return $array;
    }


    /**
     * 对数组进行过滤，移除数组中指定的值
     *
     * @param array $array 需要过滤的数组
     * @param array $filterValue 要移除的值的数组，默认为[null]，即默认移除null值
     * @return array 过滤后的数组
     */
    public static function filter(array $array, array $filterValue = [null])
    {
        // 遍历数组，使用引用以便直接修改原数组
        foreach ($array as $k => $item) {
            // 检查当前项是否在要移除的值的数组中
            if (in_array($item, $filterValue, true)) {
                // 如果是，移除该项
                unset($array[$k]);
            }
        }

        // 返回过滤后的数组
        return $array;
    }

    /**
     * @note 判断数组是否是一位数组
     * @param $array
     * @return bool
     */
    public static function isOneDimensionalArray($array) : bool
    {
        return count(array_filter($array, 'is_array')) === 0;
    }
}