<?php
namespace Helpers\Outil;

class ArrayHelper
{

    /**
     * Similar to array_column($a, null, $path) except it
     * allow mutiple items in $a to have the same key
     * @param array<array<mixed>> $a
     * @param string|callable $path
     * @return array<array<array<mixed>>> indexed using $path
     */
    public static function indexBy(array $a, $path) :array
    {
        $m = [];
        foreach ($a as $k => $o) {
            if (is_callable($path)) {
                $key = $path($o, $k);
            } else {
                $key = $o[ $path ];
            }
            
            $m[$key][] = $o;
        }
        return $m;
    }

    // /**
    //  * Similar to array_column($a, null, $path) except it
    //  * allow mutiple items in $a to have the same key
    //  * @template Type of array<mixed>|object
    //  * @param array<Type> $a
    //  * @param string|callable $path
    //  * @return array<array<Type>> indexed using $path
    //  */
    // public static function indexBy(array $a, $path) :array
    // {
    //     $m = [];
    //     foreach ($a as $o) {
    //         if (is_callable($path)) {
    //             $key = $path($o);
    //         } else {
    //             $key = (is_object($o)) ? $o->{$path} : $o[ $path ];
    //         }
            
    //         $m[$key][] = $o;
    //     }
    //     return $m;
    // }

    /**
     * Similar to array_column($a, null, $path) except it
     * allow for callback
     * @template Type of mixed
     * @param array<Type> $a
     * @param callable $fn
     * @return array<Type> indexed using $path
     */
    public static function indexUniqBy(array $a, $fn) :array
    {
        $m = [];
        foreach ($a as $o) {
            $key = $fn($o);
            $m[$key] = $o;
        }
        return $m;
    }
    
    /**
     * refactorArray
     *
     * @param  array<mixed> $array
     * @return array<mixed>
     * @deprecated use php native array_values intead
     */
    public static function refactorArray(array $array) : array
    {
        $arrayTmp = [];

        foreach ($array as $val) {
            $arrayTmp[] = $val;
        }
        
        return $arrayTmp;
    }

    /**
     * @param array<mixed> $array
     * @param array<mixed>|callable $listKey
     * @return array<mixed>
     */
    public static function filterByKey(array $array, $listKey) : array
    {
        if (is_callable($listKey)) {
            $listKey = array_filter(array_keys($array), $listKey);
        }
        return array_intersect_key(
            $array,
            array_flip($listKey)
        );
    }

    /**
     * @param array<mixed> $array
     * @return mixed|null
     */
    public static function findFirst(array $array, callable $fnFilter)
    {
        foreach ($array as $item) {
            if ($fnFilter($item)) {
                return $item;
            }
        }
        return null;
    }

    /**
     * Sort an array using a given secondary array
     * @param array<mixed> $array
     * @param array<int|string> $order lsit of field in the desired order
     * @return array<mixed>
     */
    public static function sortKeyByArray(array $array, array $order)
    {
        $res = [];
        foreach ($order as $key) {
            if (! array_key_exists($key, $array)) {
                continue;
            }
            $res[ $key ] = $array[ $key ];
        }
        return $res;
    }

    /**
     *
     * @param array<mixed> $arr
     * @param array<string|int> $path list of path to take
     * @param array{'??'?:mixed} $option
     * @return mixed
     */
    public static function pathAccess(array $arr, array $path, array $option = [])
    {
        $tmp = $arr;
        foreach ($path as $field) {
            if (is_array($tmp) && array_key_exists($field, $tmp)) {
                $tmp = $tmp[ $field ];
                continue;
            }
            // Coalesce
            if (array_key_exists('??', $option)) {
                return $option['??'] ?? null;
            }
            throw new \Exception('Invalid path "'.var_export($path, true).'" on "'.$field.'"');
        }
        return $tmp;
    }
    
    /**
     * Parse array input key "toto[titi][tata]" into access path [toto,titi,tata]
     * @return array<string>
     */
    public static function getAccessPath(string $key) :array
    {
        $start = strpos($key, '[');
        if ($start === false) {
            return [$key];
        }

        preg_match_all('/\\[(.*?)\\]/', $key, $match);
        if (empty($match[1])) {
            throw new \Exception('Invalid key "'.$key.'"');
        }

        return array_merge([substr($key, 0, $start)], $match[1]);
    }

    /**
     * Flatten recursively
     * @param array<mixed> $array
     * @return array<mixed>
     */
    public static function flatten(
        array $array,
        string $seperator = '.',
        string $prefix = ''
    ) :array {
        $return = [];
        foreach ($array as $key => $value) {
            if (is_array($value)) {
                $return = array_merge(
                    $return,
                    self::flatten(
                        $value,
                        $seperator,
                        $prefix . $key . $seperator
                    )
                );
                continue;
            }
            $return[$prefix . $key] = $value;
        }
        return $return;
    }

    /**
     * Flatten non-recursively
     * @param mixed $array
     * @return array<mixed>
     */
    public static function flattenOnce(
        $array,
        string $seperator = '.'
    ) :array {
        $return = [];
        if (is_array($array)) {
            foreach ($array as $parentKey => $subarray) {
                if (!is_array($subarray)) {
                    $return[$parentKey] = $subarray;
                    continue;
                }
                foreach ($subarray as $key => $value) {
                    $return[$parentKey . $seperator . $key] = $value;
                }
            }
        }
        return $return;
    }

    /**
     * Similar to array_flip except collision are handle using a depth
     * @param array<mixed> $arr
     * @return array<array<mixed>>
     */
    public static function flip(array $arr)
    {
        $result = [];
        foreach ($arr as $k => $v) {
            $result[$v][] = $k;
        }
        return $result;
    }

    /**
     * Summary of joinManyToMany
     * @param array<array<mixed>> $listParent
     * @param array<array<mixed>> $listChild
     * @return array<array<mixed>>
     */
    public static function joinManyToMany(
        array $listParent,
        array $listChild,
        string $colKey,
        string $resultfieldName,
        ?string $childColkey = null
    ) :array {
        $mapChild = ArrayHelper::indexBy($listChild, $childColkey ?? $colKey);

        foreach ($listParent as &$entity) { // TODO: this should not affect input $listParent
            $entity[ $resultfieldName ] = $mapChild[ $entity[$colKey] ] ?? [];
        }
        return $listParent;
    }

    /**
     * @param array<mixed> $entity
     * @param array<mixed> $delta
     * @return array<mixed>
     */
    public static function mergeDelta(array $entity, array $delta) :array
    {
        foreach ($entity as $k => $v) {
            if (! array_key_exists($k, $delta)) {
                continue;
            }

            $value = $delta[ $k ] ?? null;
            if (empty($value)) {
                continue;
            }

            if (is_array($v)) {
                // TODO : handle item identifier
                $entity[ $k ] = array_merge(
                    $v,
                    $value
                );
                continue;
            }

            $entity[ $k ] = $value;
        }
        return $entity;
    }

    /**
     * @param mixed $subject
     * @param array<callable> $listCallable
     * @return mixed
     */
    public static function chain($subject, array $listCallable)
    {
        return array_reduce($listCallable, function ($carry, callable $fn) {
            return $fn($carry);
        }, $subject);
    }

    /**
     * @param array<mixed> $subject
     * @return array<mixed>
     */
    public static function uncolumn(array $subject, string $keyField, string $valueField)
    {
        $tmp = [];
        foreach ($subject as $k => $v) {
            $tmp[] = [
                $keyField => $k,
                $valueField => $v,
            ];
        }
        return $tmp;
    }
}
