<?php

/**
 * @package     EasyStore.Administrator
 * @subpackage  com_easystore
 * @copyright   (C) 2023 - 2024 JoomShaper. <https://www.joomshaper.com>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace JoomShaper\SPPageBuilder\DynamicContent\Supports;

defined('_JEXEC') or die;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
use Traversable;

/**
 * The array helper class.
 *
 * @since 5.5.0
 */
class Arr implements ArrayAccess, IteratorAggregate
{
    /**
     * The array items.
     *
     * @var array
     *
     * @since 5.5.0
     */
    protected $items = [];

    /**
     * The private constructor method that helps the class to instantiate from outside,
     * Instead it enforce to use the Arr::make() method to instantiate the array instance.
     *
     * @param array $items The initial array items
     *
     * @since 5.5.0
     */
    protected function __construct(array $items = [])
    {
        $this->items = $items;
    }

    /**
     * Make an instance of the Arr class.
     *
     * @param array $items The initial array items
     *
     * @return self
     *
     * @since 5.5.0
     */
    public static function make(array $items = [])
    {
        return (new self($items));
    }

    /**
     * Check if the array has the provided key
     *
     * @param string $key The key to check
     *
     * @return boolean True if the key exists, false otherwise
     *
     * @since 5.5.0
     */
    public function has($key): bool
    {
        return isset($this->items[ $key ]);
    }

    /**
     * Get the value of the array by the key.
     *
     * @param string $key The key to retrieve
     * @param mixed $default The default value if the key doesn't exist
     *
     * @return mixed The value associated with the key or the default value
     *
     * @since 5.5.0
     */
    public function get($key, $default = null)
    {
        if (!$this->has($key)) {
            return $default;
        }

        return $this->items[$key];
    }

    /**
     * Set the value into the array. If the key exists then it will update the value. Add a new one otherwise.
     *
     * @param string $key The key to set
     * @param mixed $value The value to set
     *
     * @return void
     *
     * @since 5.5.0
     */
    public function set($key, $value): void
    {
        $this->items[$key] = $value;
    }

    /**
     * Count the total number of items into the array.
     *
     * @return integer The number of items in the array
     *
     * @since 5.5.0
     */
    public function count(): int
    {
        return count($this->items);
    }

    /**
     * Add a new item at the end of the array
     *
     * @param mixed $value The value to add
     *
     * @return integer The updated array length
     *
     * @since 5.5.0
     */
    public function push($value): int
    {
        $this->items[] = $value;

        return $this->count();
    }

    /**
     * Add a new item to the beginning of the array.
     *
     * @param mixed $value The value to add
     *
     * @return integer The updated array length
     *
     * @since 5.5.0
     */
    public function prepend($value): int
    {
        array_unshift($this->items, $value);

        return $this->count();
    }

    /**
     * Remove an item from the end of the array.
     *
     * @return mixed The removed item
     *
     * @since 5.5.0
     */
    public function pop()
    {
        return array_pop($this->items);
    }

    /**
     * Remove and get an item from the beginning of the array.
     *
     * @return mixed The removed item
     *
     * @since 5.5.0
     */
    public function shift()
    {
        return array_shift($this->items);
    }

    /**
     * Pick the last element of the array.
     * If the array is empty then it will return null.
     * This will only pick the item, but does not remove it from the array.
     *
     * @return mixed The last element or null if the array is empty
     *
     * @since 5.5.0
     */
    public function top()
    {
        if ($this->count() === 0) {
            return null;
        }

        $length = $this->count();

        return $this->items[$length - 1];
    }

    /**
     * Pick the first element of the array.
     * If the array is empty then it will return null.
     * This will only pick the item, but does not remove it from the array.
     *
     * @return mixed The first element or null if the array is empty
     *
     * @since 5.5.0
     */
    public function front()
    {
        if ($this->count() === 0) {
            return null;
        }

        return $this->items[0];
    }

    /**
     * Run a map operation using a callable to the array.
     *
     * @param callable $callable The callable function
     *
     * @return self The new array after applying the callable
     *
     * @since 5.5.0
     */
    public function map(callable $callable): self
    {
        $newArray = [];

        foreach ($this->items as $index => $value) {
            $newArray[] = $callable($value, $index);
        }

        return new static($newArray);
    }

    /**
     * Iterate over each item in the array.
     *
     * @param callable $callable The callable function
     *
     * @return void
     *
     * @since 5.5.0
     */
    public function foreach(callable $callable): void
    {
        foreach ($this->items as $index => $value) {
            $callable($value, $index);
        }
    }

    /**
     * Filter the array by a callable function.
     * The function will return a true/false value and the return value is true then the value will be kept,
     * otherwise removed.
     *
     * @param callable $callable The callable function for filtering
     *
     * @return self The filtered array
     *
     * @since 5.5.0
     */
    public function filter(callable $callable): self
    {
        $filteredArray = [];

        foreach ($this->items as $index => $value) {
            $result = $callable($value, $index);

            if ($result) {
                $filteredArray[] = $value;
            }
        }

        return new static($filteredArray);
    }

    /**
     * Find an item into the array by a callable function condition.
     *
     * @param callable $callable The callable function for finding
     *
     * @return mixed|null The found item or null if not found
     *
     * @since 5.5.0
     */
    public function find(callable $callable)
    {
        foreach ($this->items as $index => $value) {
            if ($callable($value, $index)) {
                return $value;
            }
        }

        return null;
    }

    /**
     * Find the index of an item into the array.
     * If not found then it will return -1.
     *
     * @param callable $callable The callable function for finding
     *
     * @return integer The index of the found item or -1 if not found
     *
     * @since 5.5.0
     */
    public function findIndex(callable $callable): int
    {
        foreach ($this->items as $index => $value) {
            if ($callable($value, $index)) {
                return $index;
            }
        }

        return -1;
    }

    /**
     * A boolean method that checks if the array satisfies a specific condition.
     * If it satisfies for only one item then it returns true.
     *
     * @param callable $callable The callable function for checking condition
     *
     * @return boolean True if any item satisfies the condition, false otherwise
     *
     * @since 5.5.0
     */
    public function some(callable $callable): bool
    {
        foreach ($this->items as $index => $value) {
            if ($callable($value, $index)) {
                return true;
            }
        }

        return false;
    }

    public static function isAssociative(array $array): bool
    {
        return array_keys($array) !== range(0, count($array) - 1);
    }

    /**
     * A boolean method that checks if the array satisfies a specific condition.
     * If every items satisfies the condition then it will return true.
     *
     * @param callable $callable The callable function for checking condition
     *
     * @return boolean True if all items satisfy the condition, false otherwise
     *
     * @since 5.5.0
     */
    public function every(callable $callable): bool
    {
        foreach ($this->items as $index => $value) {
            if (!$callable($value, $index)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Replicate the array_reduce function for usage consistency
     *
     * @param callable $callable The callable function for reducing
     * @param mixed $initial_value The initial value for reduction
     *
     * @return self The result of the reduction
     *
     * @since 5.5.0
     */
    public function reduce(callable $callable, $initial_value)
    {
        $result = array_reduce($this->items, $callable, $initial_value);
        return new static($result);
    }

    /**
     * Flatten the array for the sequential array
     * 
     * @return array
     * @since 5.5.0
     */
    public function flatten()
    {
        return array_reduce($this->items, function ($carry, $item) {
            return is_array($item)
                ? array_merge($carry, (new static($item))->flatten())
                : array_merge($carry, [$item]);
        }, []);
    }

    /**
     * Join the array using a glue
     *
     * @param string $glue The symbol by which it will be joined
     *
     * @return string The joined string
     *
     * @since 5.5.0
     */
    public function join($glue = ',')
    {
        return implode($glue, $this->items);
    }

    /**
     * Pluck the array by a key
     *
     * @param string $key The key to pluck
     *
     * @return self The plucked array
     *
     * @since 5.5.0
     */
    public function pluck($key)
    {
        return $this->map(function($item) use ($key) {
            return is_array($item) ? $item[$key] : $item->{$key};
        });
    }

    /**
     * Group the array by a key
     *
     * @param string $key The key to group by
     *
     * @return array The grouped array
     *
     * @since 5.5.0
     */
    public function groupBy(string $key)
    {
        $grouped = $this->reduce(function($carry, $item) use ($key) {
            $carry[$item[$key]] ??= [];
            $carry[$item[$key]][] = $item;
            return $carry;
        }, [])->toArray();

        return array_values($grouped);
    }

    /**
     * Diff the associative array by the provided array
     * 
     * @param array $array The array to diff
     * 
     * @return self
     * @since 5.5.0
     */
    public function diffAssoc($array)
    {
        $array = $array instanceof self ? $array->toArray() : $array;

        return new static(
            array_diff_assoc($this->items, $array)
        );
    }

    /**
     * Diff the array by the provided array
     * 
     * @param array $array The array to diff
     * 
     * @return self
     * @since 5.5.0
     */
    public function diff($array)
    {
        $array = $array instanceof self ? $array->toArray() : $array;

        return new static(
            array_values(
                array_diff($this->items, $array)
            )
        );
    }

    /**
     * Convert the array to an array
     *
     * @return array The array
     *
     * @since 5.5.0
     */
    public function toArray(): array
    {
        return $this->isAssociative($this->items)
            ? $this->items
            : array_values($this->items);
    }

    /**
     * Checks whether an offset exists
     *
     * @param mixed $key An offset to check for
     *
     * @return bool True if the offset exists, false otherwise
     *
     * @since 5.5.0
     */
    public function offsetExists($key): bool
    {
        return $this->has($key);
    }

    /**
     * Offset to retrieve
     *
     * @param mixed $key The offset to retrieve
     *
     * @return mixed The value at the specified offset
     *
     * @since 5.5.0
     */
    public function offsetGet($key): mixed
    {
        return $this->get($key);
    }

    /**
     * Offset to set
     *
     * @param mixed $key The offset to assign the value to
     * @param mixed $value The value to set
     *
     * @return void
     *
     * @since 5.5.0
     */
    public function offsetSet($key, $value): void
    {
        $this->set($key, $value);
    }

    /**
     * Offset to unset
     *
     * @param mixed $key The offset to unset
     *
     * @return void
     *
     * @since 5.5.0
     */
    public function offsetUnset($key): void
    {
        unset($this->items[$key]);
    }

    /**
     * Get the iterator
     *
     * @return Traversable The iterator
     *
     * @since 5.5.0
     */
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->items);
    }
}
