<?php
/**
 * Jingga
 *
 * PHP Version 8.2
 *
 * @package   phpOMS\Utils
 * @copyright Dennis Eichhorn
 * @license   OMS License 2.2
 * @version   1.0.0
 * @link      https://jingga.app
 */
declare(strict_types=1);

namespace phpOMS\Utils;

/**
 * Array utils.
 *
 * @package phpOMS\Utils
 * @license OMS License 2.2
 * @link    https://jingga.app
 * @since   1.0.0
 */
final class ArrayUtils
{
    /**
     * Constructor.
     *
     * @since 1.0.0
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }

    /**
     * Check if needle exists in multidimensional array.
     *
     * @param string           $path  Path to element
     * @param array            $data  Array
     * @param non-empty-string $delim Delimiter for path
     *
     * @return array
     *
     * @throws \Exception
     *
     * @since 1.0.0
     */
    public static function unsetArray(string $path, array $data, string $delim = '/') : array
    {
        $nodes  = \explode($delim, \trim($path, $delim));
        $prevEl = null;
        $el     = &$data;
        $node   = null;

        foreach ($nodes as $node) {
            $prevEl = &$el;

            if (!isset($el[$node])) {
                break;
            }

            $el = &$el[$node];
        }

        if ($prevEl !== null) {
            unset($prevEl[$node]);
        }

        return $data;
    }

    /**
     * Calculate the range of the array
     *
     * @param int[]|float[] $values Numeric values
     *
     * @return int|float Range of the array
     *
     * @since 1.0.0
     */
    public static function range(array $values) : int|float
    {
        return \max($values) - \min($values);
    }

    /**
     * Set element in array by path
     *
     * @param string           $path      Path to element
     * @param array            $data      Array
     * @param mixed            $value     Value to add
     * @param non-empty-string $delim     Delimiter for path
     * @param bool             $overwrite Overwrite if existing
     *
     * @return array
     *
     * @throws \Exception This exception is thrown if the path is corrupted
     *
     * @since 1.0.0
     */
    public static function setArray(string $path, array $data, mixed $value, string $delim = '/', bool $overwrite = false) : array
    {
        $pathParts = \explode($delim, \trim($path, $delim));
        $current   = &$data;

        foreach ($pathParts as $key) {
            $current = &$current[$key];
        }

        if ($overwrite) {
            $current = $value;
        } elseif (\is_array($current) && !\is_array($value)) {
            $current[] = $value;
        } elseif (\is_array($current) && \is_array($value)) {
            $current = \array_merge($current, $value);
        } elseif (\is_scalar($current) && $current !== null) {
            $current = [$current, $value];
        } else {
            $current = $value;
        }

        return $data;
    }

    /**
     * Get element of array by path
     *
     * @param string           $path  Path to element
     * @param array            $data  Array
     * @param non-empty-string $delim Delimiter for path
     *
     * @return mixed
     *
     * @throws \Exception This exception is thrown if the path is corrupted
     *
     * @since 1.0.0
     */
    public static function getArray(string $path, array $data, string $delim = '/') : mixed
    {
        $pathParts = \explode($delim, \trim($path, $delim));
        $current   = $data;

        foreach ($pathParts as $key) {
            if (!isset($current[$key])) {
                return null;
            }

            $current = $current[$key];
        }

        return $current;
    }

    /**
     * Check if needle exists in multidimensional array.
     *
     * @param mixed $needle   Needle for search
     * @param array $haystack Haystack for search
     * @param mixed $key      Key that has to match (optional)
     *
     * @return bool
     *
     * @since 1.0.0
     */
    public static function inArrayRecursive(mixed $needle, array $haystack, $key = null) : bool
    {
        $found = false;

        foreach ($haystack as $k => $item) {
            if ($item === $needle && ($key === null || $key === $k)) {
                return true;
            } elseif (\is_array($item)) {
                $found = self::inArrayRecursive($needle, $item, $key);

                if ($found) {
                    return true;
                }
            }
        }

        return $found;
    }

    /**
     * Check if any of the needles are in the array
     *
     * @param array $needles  Needles for search
     * @param array $haystack Haystack for search
     *
     * @return bool
     *
     * @since 1.0.0
     */
    public static function anyInArray(array $needles, array $haystack) : bool
    {
        foreach ($needles as $needle) {
            if (\in_array($needle, $haystack)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if all of the needles are in the array
     *
     * @param array $needles  Needles for search
     * @param array $haystack Haystack for search
     *
     * @return bool
     *
     * @since 1.0.0
     */
    public static function allInArray(array $needles, array $haystack) : bool
    {
        foreach ($needles as $needle) {
            if (!\in_array($needle, $haystack)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Convert array to csv string.
     *
     * @param array  $data      Data to convert
     * @param string $delimiter Delim to use
     * @param string $enclosure Enclosure to use
     * @param string $escape    Escape to use
     *
     * @return string
     *
     * @since 1.0.0
     */
    public static function arrayToCsv(array $data, string $delimiter = ';', string $enclosure = '"', string $escape = '\\') : string
    {
        $outstream = \fopen('php://memory', 'r+');

        if ($outstream === false) {
            throw new \Exception(); // @codeCoverageIgnore
        }

        foreach ($data as $line) {
            /** @noinspection PhpMethodParametersCountMismatchInspection */
            \fputcsv($outstream, $line, $delimiter, $enclosure, $escape);
        }

        \rewind($outstream);
        $csv = \stream_get_contents($outstream);
        \fclose($outstream);

        return $csv === false ? '' : $csv;
    }

    /**
     * Convert array to xml string.
     *
     * @param array             $data Data to convert
     * @param \SimpleXMLElement $xml  XML parent
     *
     * @return string
     *
     * @since 1.0.0
     */
    public static function arrayToXml(array $data, ?\SimpleXMLElement $xml = null) : string
    {
        $xml ??= new \SimpleXMLElement('<root/>');

        foreach ($data as $key => $value) {
            if (\is_array($value)) {
                self::arrayToXml($value, $xml->addChild($key));
            } else {
                $xml->addChild($key, \htmlspecialchars($value));
            }
        }
        return (string) $xml->asXML();
    }

    /**
     * Get array value by argument id.
     *
     * Useful for parsing command line parsing
     *
     * @template T
     *
     * @param string               $id   Id to find
     * @param array<string|int, T> $args CLI command list
     *
     * @return mixed
     *
     * @since 1.0.0
     */
    public static function getArg(string $id, array $args) : mixed
    {
        $key = 0;
        if (\is_numeric($id)) {
            $key = ((int) $id) - 1;
        } else {
            if (($key = \array_search($id, $args)) === false || $key === \count($args) - 1) {
                return null;
            }

            $key  = (int) $key;
            $args = \array_values($args);
        }

        $value = $args[$key + 1] ?? null;

        return \is_string($value) ? \trim($value, '\'" ') : $value;
    }

    /**
     * Check if flag is set
     *
     * @param string                   $id   Id to find
     * @param array<string|int, mixed> $args CLI command list
     *
     * @return int
     *
     * @since 1.0.0
     */
    public static function hasArg(string $id, array $args) : int
    {
        return ($key = \array_search($id, $args)) === false
            ? -1
            : (int) $key;
    }

    /**
     * Flatten array
     *
     * Reduces multi dimensional array to one dimensional array. Flatten tries to maintain the index as far as possible.
     *
     * @param array $array Multi dimensional array to flatten
     *
     * @return array
     *
     * @since 1.0.0
     */
    public static function arrayFlatten(array $array) : array
    {
        // see collection collapse as alternative?!
        $flat  = [];
        $stack = \array_values($array);

        while (!empty($stack)) {
            $value = \array_shift($stack);

            if (\is_array($value)) {
                $stack = \array_merge(\array_values($value), $stack);
            } else {
                $flat[] = $value;
            }
        }

        return $flat;
    }

    /**
     * Sum of array elements
     *
     * @param array $array Array to sum
     * @param int   $start Start index
     * @param int   $count Amount of elements to sum
     *
     * @return int|float
     *
     * @since 1.0.0
     */
    public static function arraySum(array $array, int $start = 0, int $count = 0) : int | float
    {
        $count = $count === 0 ? \count($array) : $start + $count;
        $sum   = 0;
        $array = \array_values($array);

        for ($i = $start; $i <= $count - 1; ++$i) {
            $sum += $array[$i];
        }

        return $sum;
    }

    /**
     * Sum multi dimensional array
     *
     * @param array $array Multi dimensional array to flatten
     *
     * @return mixed
     *
     * @since 1.0.0
     */
    public static function arraySumRecursive(array $array) : mixed
    {
        return \array_sum(self::arrayFlatten($array));
    }

    /**
     * Applying abs to every array value
     *
     * @param array<int|float> $values Numeric values
     *
     * @return array<int|float>
     *
     * @since 1.0.0
     */
    public static function abs(array $values) : array
    {
        $abs = [];

        foreach ($values as $value) {
            $abs[] = \abs($value);
        }

        return $abs;
    }

    /**
     * Power all values in array.
     *
     * @param array<int|float> $values Values to square
     * @param float            $exp    Exponent
     *
     * @return float[]
     *
     * @since 1.0.0
     */
    public static function power(array $values, int | float $exp = 2) : array
    {
        $squared = [];

        foreach ($values as $value) {
            $squared[] = $value ** $exp;
        }

        return $squared;
    }

    /**
     * Get a column of 2D matrix.
     *
     * @param array<array> $matrix Matrix
     * @param mixed        $index  Column index to return
     *
     * @return array
     *
     * @since 1.0.0
     */
    public static function column(array $matrix, mixed $index) : array
    {
        $column = [];
        foreach ($matrix as $id => $row) {
            $column[$id] = $row[$index];
        }

        return $column;
    }

    /**
     * Sqrt all values in array.
     *
     * @param array<int|float> $values Values to sqrt
     *
     * @return array<int|float>
     *
     * @since 1.0.0
     */
    public static function sqrt(array $values) : array
    {
        $squared = [];

        foreach ($values as $value) {
            $squared[] = \sqrt($value);
        }

        return $squared;
    }

    /**
     * Get the associative difference of two arrays.
     *
     * @param array $values1 Array 1
     * @param array $values2 Array 2
     *
     * @return array
     *
     * @since 1.0.0
     */
    public static function array_diff_assoc_recursive(array $values1, array $values2) : array
    {
        $diff = [];
        foreach ($values1 as $key => $value) {
            if (\is_array($value)) {
                if (!\array_key_exists($key, $values2) || !\is_array($values2[$key])) {
                    $diff[$key] = $value;
                } else {
                    $subDiff = self::array_diff_assoc_recursive($value, $values2[$key]);
                    if (!empty($subDiff)) {
                        $diff[$key] = $subDiff;
                    }
                }
            } elseif (!\array_key_exists($key, $values2) || $values2[$key] !== $value) {
                $diff[$key] = $value;
            }
        }

        return $diff;
    }
}
