<?php

/**
 * Хелпер для роботи з масивами
 * Розширені методи для маніпуляції масивами
 *
 * @package Flowaxy\Support\Helpers
 * @version 1.0.0 Alpha prerelease
 */

declare(strict_types=1);

namespace Flowaxy\Support\Helpers;

final class ArrayHelper
{
    /**
     * Отримання значення з масиву за ключем з підтримкою dot-нотації
     *
     * @param array<string, mixed> $array
     * @param string|int|null $key
     * @param mixed $default
     * @return mixed
     */
    public static function get(array $array, string|int|null $key, mixed $default = null): mixed
    {
        if ($key === null) {
            return $array;
        }

        if (array_key_exists($key, $array)) {
            return $array[$key];
        }

        if (!str_contains((string)$key, '.')) {
            return $default;
        }

        foreach (explode('.', (string)$key) as $segment) {
            if (is_array($array) && array_key_exists($segment, $array)) {
                $array = $array[$segment];
            } else {
                return $default;
            }
        }

        return $array;
    }

    /**
     * Встановлення значення в масив за ключем з підтримкою dot-нотації
     *
     * @param array<string, mixed> $array
     * @param string|int $key
     * @param mixed $value
     * @return array<string, mixed>
     */
    public static function set(array &$array, string|int $key, mixed $value): array
    {
        $keys = explode('.', (string)$key);
        $current = &$array;

        foreach ($keys as $k) {
            if (!isset($current[$k]) || !is_array($current[$k])) {
                $current[$k] = [];
            }
            $current = &$current[$k];
        }

        $current = $value;

        return $array;
    }

    /**
     * Перевірка наявності ключа в масиві з підтримкою dot-нотації
     *
     * @param array<string, mixed> $array
     * @param string|int $key
     * @return bool
     */
    public static function has(array $array, string|int $key): bool
    {
        if (array_key_exists($key, $array)) {
            return true;
        }

        if (!str_contains((string)$key, '.')) {
            return false;
        }

        foreach (explode('.', (string)$key) as $segment) {
            if (is_array($array) && array_key_exists($segment, $array)) {
                $array = $array[$segment];
            } else {
                return false;
            }
        }

        return true;
    }

    /**
     * Витягнути значення з масиву за ключем
     *
     * @param array<int, array<string, mixed>> $array
     * @param string|int $key
     * @return array<int, mixed>
     */
    public static function pluck(array $array, string|int $key): array
    {
        $results = [];

        foreach ($array as $item) {
            if (is_array($item) && isset($item[$key])) {
                $results[] = $item[$key];
            } elseif (is_object($item) && isset($item->$key)) {
                $results[] = $item->$key;
            }
        }

        return $results;
    }

    /**
     * Створити масив з ключами з одного поля
     *
     * @param array<int, array<string, mixed>> $array
     * @param string|int $key
     * @return array<string|int, array<string, mixed>>
     */
    public static function keyBy(array $array, string|int $key): array
    {
        $results = [];

        foreach ($array as $item) {
            if (is_array($item) && isset($item[$key])) {
                $results[$item[$key]] = $item;
            } elseif (is_object($item) && isset($item->$key)) {
                $results[$item->$key] = $item;
            }
        }

        return $results;
    }

    /**
     * Групувати масив за ключем
     *
     * @param array<int, array<string, mixed>> $array
     * @param string|int $key
     * @return array<string|int, array<int, array<string, mixed>>>
     */
    public static function groupBy(array $array, string|int $key): array
    {
        $results = [];

        foreach ($array as $item) {
            $groupKey = null;
            if (is_array($item) && isset($item[$key])) {
                $groupKey = $item[$key];
            } elseif (is_object($item) && isset($item->$key)) {
                $groupKey = $item->$key;
            }

            if ($groupKey !== null) {
                $results[$groupKey][] = $item;
            }
        }

        return $results;
    }

    /**
     * Розгорнути багатовимірний масив в одновимірний
     *
     * @param array<int|string, mixed> $array
     * @param int $depth
     * @return array<int|string, mixed>
     */
    public static function flatten(array $array, int $depth = INF): array
    {
        $result = [];

        foreach ($array as $item) {
            if (is_array($item) && $depth > 0) {
                $result = array_merge($result, self::flatten($item, $depth - 1));
            } else {
                $result[] = $item;
            }
        }

        return $result;
    }

    /**
     * Конвертувати масив в dot-нотацію
     *
     * @param array<string, mixed> $array
     * @param string $prepend
     * @return array<string, mixed>
     */
    public static function dot(array $array, string $prepend = ''): array
    {
        $results = [];

        foreach ($array as $key => $value) {
            if (is_array($value) && !empty($value)) {
                $results = array_merge($results, self::dot($value, $prepend . $key . '.'));
            } else {
                $results[$prepend . $key] = $value;
            }
        }

        return $results;
    }

    /**
     * Конвертувати dot-нотацію назад в масив
     *
     * @param array<string, mixed> $array
     * @return array<string, mixed>
     */
    public static function undot(array $array): array
    {
        $results = [];

        foreach ($array as $key => $value) {
            self::set($results, $key, $value);
        }

        return $results;
    }

    /**
     * Отримати всі ключі крім вказаних
     *
     * @param array<string, mixed> $array
     * @param array<int, string|int> $keys
     * @return array<string, mixed>
     */
    public static function except(array $array, array $keys): array
    {
        return array_diff_key($array, array_flip($keys));
    }

    /**
     * Отримати тільки вказані ключі
     *
     * @param array<string, mixed> $array
     * @param array<int, string|int> $keys
     * @return array<string, mixed>
     */
    public static function only(array $array, array $keys): array
    {
        return array_intersect_key($array, array_flip($keys));
    }

    /**
     * Отримати перший елемент масиву
     *
     * @param array<int|string, mixed> $array
     * @param callable|null $callback
     * @param mixed $default
     * @return mixed
     */
    public static function first(array $array, ?callable $callback = null, mixed $default = null): mixed
    {
        if ($callback === null) {
            if (empty($array)) {
                return $default;
            }
            return reset($array);
        }

        foreach ($array as $key => $value) {
            if ($callback($value, $key)) {
                return $value;
            }
        }

        return $default;
    }

    /**
     * Отримати останній елемент масиву
     *
     * @param array<int|string, mixed> $array
     * @param callable|null $callback
     * @param mixed $default
     * @return mixed
     */
    public static function last(array $array, ?callable $callback = null, mixed $default = null): mixed
    {
        if ($callback === null) {
            if (empty($array)) {
                return $default;
            }
            return end($array);
        }

        return self::first(array_reverse($array, true), $callback, $default);
    }

    /**
     * Отримати випадковий елемент з масиву
     *
     * @param array<int|string, mixed> $array
     * @param int|null $number
     * @return mixed|array<int|string, mixed>
     */
    public static function random(array $array, ?int $number = null): mixed
    {
        $requested = $number ?? 1;
        $count = count($array);

        if ($requested > $count) {
            throw new \InvalidArgumentException("You requested {$requested} items, but there are only {$count} items available.");
        }

        if ($number === null) {
            return $array[array_rand($array)];
        }

        $keys = array_rand($array, $number);
        if ($number === 1) {
            return $array[$keys];
        }

        return array_intersect_key($array, array_flip((array)$keys));
    }

    /**
     * Обгорнути значення в масив
     *
     * @param mixed $value
     * @return array<int, mixed>
     */
    public static function wrap(mixed $value): array
    {
        if (is_null($value)) {
            return [];
        }

        return is_array($value) ? $value : [$value];
    }

    /**
     * Об'єднати масиви в один
     *
     * @param array<int|string, mixed> ...$arrays
     * @return array<int|string, mixed>
     */
    public static function collapse(array ...$arrays): array
    {
        $results = [];

        foreach ($arrays as $array) {
            $results = array_merge($results, $array);
        }

        return $results;
    }

    /**
     * Додати елемент на початок масиву
     *
     * @param array<int|string, mixed> $array
     * @param mixed $value
     * @param string|int|null $key
     * @return array<int|string, mixed>
     */
    public static function prepend(array $array, mixed $value, string|int|null $key = null): array
    {
        if ($key === null) {
            array_unshift($array, $value);
        } else {
            $array = [$key => $value] + $array;
        }

        return $array;
    }

    /**
     * Додати елемент в кінець масиву
     *
     * @param array<int|string, mixed> $array
     * @param mixed $value
     * @param string|int|null $key
     * @return array<int|string, mixed>
     */
    public static function push(array $array, mixed $value, string|int|null $key = null): array
    {
        if ($key === null) {
            $array[] = $value;
        } else {
            $array[$key] = $value;
        }

        return $array;
    }

    /**
     * Витягнути та видалити елемент з масиву
     *
     * @param array<string, mixed> $array
     * @param string|int $key
     * @param mixed $default
     * @return mixed
     */
    public static function pull(array &$array, string|int $key, mixed $default = null): mixed
    {
        $value = self::get($array, $key, $default);
        self::forget($array, $key);

        return $value;
    }

    /**
     * Видалити елемент з масиву за ключем
     *
     * @param array<string, mixed> $array
     * @param string|int|array<int, string|int> $keys
     * @return void
     */
    public static function forget(array &$array, string|int|array $keys): void
    {
        $keys = is_array($keys) ? $keys : [$keys];

        foreach ($keys as $key) {
            if (array_key_exists($key, $array)) {
                unset($array[$key]);
                continue;
            }

            $parts = explode('.', (string)$key);
            $current = &$array;

            while (count($parts) > 1) {
                $part = array_shift($parts);

                if (isset($current[$part]) && is_array($current[$part])) {
                    $current = &$current[$part];
                } else {
                    continue 2;
                }
            }

            unset($current[array_shift($parts)]);
        }
    }

    /**
     * Перемішати масив
     *
     * @param array<int|string, mixed> $array
     * @return array<int|string, mixed>
     */
    public static function shuffle(array $array): array
    {
        shuffle($array);

        return $array;
    }

    /**
     * Сортувати масив
     *
     * @param array<int|string, mixed> $array
     * @param callable|null $callback
     * @param int $flags
     * @return array<int|string, mixed>
     */
    public static function sort(array $array, ?callable $callback = null, int $flags = SORT_REGULAR): array
    {
        if ($callback !== null) {
            uasort($array, $callback);
        } else {
            asort($array, $flags);
        }

        return $array;
    }

    /**
     * Отримати унікальні значення з масиву
     *
     * @param array<int|string, mixed> $array
     * @param string|callable|null $key
     * @return array<int|string, mixed>
     */
    public static function unique(array $array, string|callable|null $key = null): array
    {
        if ($key === null) {
            return array_unique($array, SORT_REGULAR);
        }

        $seen = [];
        $results = [];

        foreach ($array as $item) {
            $keyValue = is_callable($key) ? $key($item) : (is_array($item) ? ($item[$key] ?? null) : null);

            if (!in_array($keyValue, $seen, true)) {
                $seen[] = $keyValue;
                $results[] = $item;
            }
        }

        return $results;
    }
}
