<?php

namespace Pionia\Pionia\Utils;

class Arrayable
{
    use Microable;

    private array $array = [];

    public function __construct(array $array = [])
    {
        if ($this->arrayType($array) === 'indexed') {
            foreach ($array as $key => $value) {
                $this->array[(string)$key] = $value;
            }
        } else {
            $this->array = $array;
        }
    }

    /**
     * Checks if an array is associative
     * @param array|null $arr
     * @return bool
     */
    public function isAssoc(?array $arr = null): bool
    {
        $arr = $arr ?? $this->array;
        return array_keys($arr) !== range(0, count($arr) - 1);
    }

    /**
     * Checks if an array has both numeric and string keys
     * @param array|null $arr
     * @return bool
     */
    public function isMixed(?array $arr = null): bool
    {
        $arr = $arr ?? $this->array;
        $hasNumericKey = false;
        $hasStringKey = false;

        foreach ($arr as $key => $value) {
            if (is_int($key)) {
                $hasNumericKey = true;
            } elseif (is_string($key)) {
                $hasStringKey = true;
            }

            if ($hasNumericKey && $hasStringKey) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get the type of the array
     * @param array|null $arr
     * @return string
     */
    public function arrayType(?array $arr = null): string
    {
        $arr = $arr ?? $this->array;
        if ($this->isMixed($arr)) {
            return 'mixed';
        } else if ($this->isAssoc($arr)) {
            return 'associative';
        }
        return 'indexed';
    }

    /**
     * Get the keys of the array
     * @return array
     */
    public function keys(): array
    {
        return array_keys($this->array);
    }

    /**
     * Get the values of the array
     * @return array
     */
    public function values(): array
    {
        return array_values($this->array);
    }

    /**
     * Map through the array
     * @param callable $callback
     * @return Arrayable
     */
    public function map(callable $callback): static
    {
        $this->array = array_map($callback, $this->array);
        return $this;
    }

    /**
     * Filter the array
     * @param callable $callback
     * @return Arrayable
     */
    public function filter(callable $callback): static
    {
        $this->array = array_filter($this->array, $callback);
        return $this;
    }

    /**
     * Reduce the array
     * @param callable $callback
     * @param null $initial
     * @return Arrayable
     */
    public function reduce(callable $callback, mixed $initial = null): static
    {
        $this->array = array_reduce($this->array, $callback, $initial);
        return $this;
    }

    public function each(callable $callback): static
    {
        foreach ($this->array as $key => $value) {
            $callback($value, $key);
        }
        return $this;
    }

    public function sort(callable $callback): static
    {
        usort($this->array, $callback);
        return $this;
    }

    public function sortKeys(callable $callback): static
    {
        uksort($this->array, $callback);
        return $this;
    }

    public function sortValues(callable $callback): static
    {
        uasort($this->array, $callback);
        return $this;
    }

    public function reverse(): static
    {
        $this->array = array_reverse($this->array);
        return $this;
    }

    public function slice(int $offset, int $length = null, $preserve_keys = false): static
    {
        $this->array = array_slice($this->array, $offset, $length, $preserve_keys);
        return $this;
    }

    public function chunk(int $size, bool $preserve_keys = false): static
    {
        $this->array = array_chunk($this->array, $size, $preserve_keys);
        return $this;
    }

    public function keysToLowerCase(): static
    {
        $this->array = array_change_key_case($this->array, CASE_LOWER);
        return $this;
    }

    public function keysToUpperCase(): static
    {
        $this->array = array_change_key_case($this->array, CASE_UPPER);
        return $this;
    }


    public function valuesToLowerCase(): static
    {
        $this->array = array_map('strtolower', $this->array);
        return $this;
    }

    /**
     * Convert the arrayable to an array
     * @return array
     */
    public function toArray(): array
    {
        return $this->array;
    }

    public function first(): mixed
    {
        $item = reset($this->array);
        return $item === false ? null : $item;
    }

    public function last()
    {
        return end($this->array);
    }

    /**
     * Convert the arrayable to json
     * @return string
     */
    public function toJson(): string
    {
        return json_encode($this->array);
    }

    public function __toString(): string
    {
        return $this->toJson();
    }

    /**
     * Get an item from the array
     *
     * If checkUpperAndLower is set to true, the method will check for uppercase and lowercase cases
     * @param ?string $name
     * @param ?bool $checkUpperAndLower
     * @return mixed
     */
    public function get(?string $name, ?bool $checkUpperAndLower = true): mixed
    {
        if (!isset($name)) {
            return $this->toArray();
        }

        if ($this->has($name)) {
            return $this->array[$name];
        }

        // check when the name is in uppercase or lowercase
        if ($checkUpperAndLower) {
            if ($this->has(strtoupper($name))) {
                return $this->array[strtoupper($name)];
            }

            if ($this->has(strtolower($name))) {
                return $this->array[strtolower($name)];
            }
        }
        return null;
    }

    /**
     * Get the size of the array
     * @return int
     */
    public function size(): int
    {
        return count($this->array);
    }

    /**
     * Checks if the array is empty
     * @return bool
     */
    public function isEmpty(): bool
    {
        return $this->size() === 0;
    }

    public function isFilled(): bool
    {
        return !$this->isEmpty();
    }

    /**
     * Add a key value pair to the array
     * If only the key is provided, the key will be the value
     * @param string $key
     * @param mixed|null $value
     * @return Arrayable
     */
    public function add(string $key, mixed $value = null): static
    {
        if (!isset($value)) {
            $this->array[$key] = $key;
        }else {
            $this->array[$key] = $value;
        }
        return $this;
    }

    /**
     * Remove an item from the array
     * @param string $key
     * @return Arrayable
     */
    public function remove($key): static
    {
        unset($this->array[$key]);
        return $this;
    }

    /**
     * Check if the array has a key
     * @param string $key
     * @return bool
     */
    public function has($key): bool
    {
        return isset($this->array[$key])
            || isset($this->array[strtolower($key)])
            || isset($this->array[strtoupper($key)])
            || array_key_exists($key, $this->array)
            || array_key_exists(strtolower($key), $this->array)
            || array_key_exists(strtoupper($key), $this->array);
    }

    public function __get($name)
    {
        return $this->get($name);
    }

    public function __set($name, $value)
    {
        $this->add($name, $value);
    }

    public function __isset($name)
    {
        return $this->has($name);
    }

    public function __unset($name)
    {
        return $this->remove($name);
    }

    /**
     * Merge a whole into the current array
     * @param array $array
     * @return Arrayable
     */
    public function merge(array $array): static
    {
        $this->array = array_merge($this->array, $array);
        return $this;
    }

    public static function toArrayable(array $array): Arrayable
    {
        return new Arrayable($array);
    }

    public function addAfter($positionKey, $key, $value = null): static
    {
        $array = [];
        foreach ($this->array as $k => $v) {
            $array[$k] = $v;
            if ($k === $positionKey) {
                $array[$key] = $value ?? $key;
            }
        }
        $this->array = $array;
        return $this;
    }

    public function addBefore(string $positionKey, string $key, mixed $value = null): static
    {
        $array = [];
        foreach ($this->array as $k => $v) {
            if ($k === $positionKey) {
                $array[$key] = $value ?? $key;
            }
            $array[$k] = $v;
        }
        $this->array = $array;
        return $this;
    }

    public function replace(string $key, mixed $value = null): static
    {
        $this->array[$key] = $value ?? $key;
        return $this;
    }

    public function shift(): mixed
    {
        return array_shift($this->array);
    }

    public function pop(): mixed
    {
        return array_pop($this->array);
    }

    public function unshift(array $value): static
    {
        if ($this->arrayType($value) === 'indexed') {
            $arr = [];
            foreach ($value as $key) {
                $arr[(string)$key] = $key;
            }
        } else {
            $arr = $value;
        }
        $this->array = array_merge($arr, $this->array);
        return $this;
    }

    public function all(): array
    {
        return $this->toArray();
    }

    public function flush(): static
    {
        $this->array = [];
        return $this;
    }

    public function at(int $index): mixed
    {
        $keys = array_keys($this->array);
        return $this->array[$keys[$index]] ?? null;
    }
}
