<?php

namespace Velis;

use ArrayObject;
use Collator;
use InvalidArgumentException;
use Traversable;
use Velis\Model\DataObject;

/**
 * Static arrays functions lib
 * @author Olek Procki <olo@velis.pl>
 */
class Arrays
{
    /**
     * Array with single null value
     * @var array
     */
    public const NULL_ARRAY = [null];


    /**
     * Checks if collection or array contains a column
     *
     * @param array $source
     * @param string $column
     *
     * @return bool
     */
    public static function hasColumn(array $source, string $column): bool
    {
        $sourceItem = reset($source);

        if (is_array($sourceItem) || $sourceItem instanceof ArrayObject) {
            // for collections
            $source = $sourceItem instanceof ArrayObject ? (array) $sourceItem : $sourceItem;
        }
        return array_key_exists($column, $source);
    }


    /**
     * converts array column values to one dimension array
     *
     * @param array $source    source array
     * @param mixed $column    key
     * @param mixed $assocKey  key for result assoc array
     * @param bool  $filter    perform array_filter on result if true
     *
     * @throws Exception
     *
     * @return array
     */
    public static function getColumn($source, $column, $assocKey = null, $filter = true)
    {
        if (!(is_array($source) || $source instanceof ArrayObject) || !sizeof($source)) {
            return array();
        }

        if (is_array($column)) {
            $colValues = array();

            foreach ($column as $singleCol) {
                $colValues = array_merge(
                    $colValues,
                    self::getColumn($source, $singleCol, $assocKey, $filter)
                );
            }

            return $colValues;
        } else {
            $sourceAsArray = (array) $source;
            if (!array_key_exists($column, (array) reset($sourceAsArray))) {
                Exception::raise('No such column in array (' . $column . ')!');
                return [];
            }

            if ($assocKey != null) {
                if (!array_key_exists($assocKey, (array) reset($sourceAsArray))) {
                    Exception::raise('No such column in array (' . $assocKey . ')!');
                    return [];
                }
            }

            $newArr = array_column($sourceAsArray, $column, $assocKey);

            if ($filter) {
                $newArr = array_filter($newArr);
            }

            return $newArr;
        }
    }


    /**
     * Returns values of multiple columns as flat array
     *
     * @param array|ArrayObject $source
     *
     * @throws Exception
     *
     * @return array
     */
    public static function getColumns($source)
    {
        if (func_num_args() > 1) {
            $args = func_get_args();
            array_shift($args);

            return self::getColumn($source, $args);
        } else {
            return array();
        }
    }


    /**
     * Gets elements by field value
     *
     * @param array|ArrayObject $source
     * @param string|array $field
     * @param mixed $value
     *
     * @return array
     */
    public static function byValue(&$source, $field, $value = null)
    {
        $result = [];

        foreach ($source as $key => $elem) {
            if (!is_array($field)) {
                if (!array_key_exists($field, Arrays::toArray($elem))) {
                    continue;
                }

                if (is_array($elem[$field]) && in_array($value, $elem[$field])) {
                    $result[$key] = $elem;
                } elseif (!is_array($elem[$field]) && ($elem[$field] == $value || (is_array($value) && in_array($elem[$field], $value)))) {
                    $result[$key] = $elem;
                }
            } else {
                foreach ($field as $singleField => $singleValue) {
                    if (!array_key_exists($singleField, Arrays::toArray($elem))) {
                        continue(2);
                    }

                    if ((is_array($elem[$singleField]) && !is_array($singleValue)) && !in_array($singleValue, $elem[$singleField])) {
                        continue(2);
                    } elseif ((is_array($elem[$singleField]) && is_array($singleValue)) && !array_intersect($singleValue, $elem[$singleField])) {
                        continue(2);
                    } elseif (!is_array($elem[$singleField]) && is_array($singleValue) && !in_array($elem[$singleField], $singleValue)) {
                        continue(2);
                    } elseif (!is_array($elem[$singleField]) && !is_array($singleValue) && $elem[$singleField] != $singleValue) {
                        continue(2);
                    }
                }
                $result[$key] = $elem;
            }
        }
        return $result;
    }


    /**
     * Gets elements by any field value
     *
     * @param array|ArrayObject $source
     * @param string|array $values
     * @return array
     */
    public static function byAnyValue(&$source, $values)
    {
        $result = array();

        foreach ($source as $key => $elem) {
            foreach ($values as $field => $value) {
                if ($elem[$field] == $value) {
                    $result[$key] = $elem;
                    continue(2);
                }
            }
        }

        return $result;
    }


    /**
     * Finds single element in array by field value
     *
     * @param array|ArrayObject $source
     * @param string|array $field
     * @param mixed $value
     *
     * @return mixed
     */
    public static function find($source, $field, $value = null)
    {
        return self::getFirst(
            self::byValue($source, $field, $value)
        );
    }


    /**
     * Extract fields from array
     *
     * @param array|ArrayObject $source
     * @param array $fields
     * @param bool $notNull
     * @param bool $unset
     *
     * @return array
     *
     * @throws InvalidArgumentException
     */
    public static function extractFields(&$source, array $fields, $notNull = false, $unset = false)
    {
        $extracted = [];

        if ($source) {
            if (!is_array($source) && !($source instanceof ArrayObject)) {
                throw new InvalidArgumentException('Parameter $source in ' . __METHOD__ . ' must be array or ArrayObject');
            }
            foreach ($source as $key => $val) {
                if (in_array($key, $fields)) {
                    if ($notNull) {
                        if ($val !== '' && $val !== null) {
                            $extracted[$key] = $val;
                        }
                    } else {
                        $extracted[$key] = $val;
                    }

                    if ($unset) {
                        unset($source[$key]);
                    }
                }
            }

            return $extracted;
        } else {
            return [];
        }
    }


    /**
     * Filter array but keep zeros
     * @param array $source
     * @return array
     */
    public static function filterKeepZeros($source)
    {
        if (is_array($source)) {
            return array_filter($source, function ($value) {
                return ($value || $value === '0' || $value === 0 || $value === 0.0);
            });
        } else {
            return $source;
        }
    }


    /**
     * Unsets array element by key(s)
     *
     * @param array|ArrayObject $source
     * @param mixed $key
     *
     * @return array|ArrayObject
     */
    public static function unsetByKey(&$source, $key)
    {
        if (!is_array($key)) {
            unset($source[$key]);
        } else {
            foreach ($key as $singleKey) {
                unset($source[$singleKey]);
            }
        }

        return $source;
    }


    /**
     * Unsets recursive array elements (ArrayObject instances) by another objects array
     *
     * @param array $source
     * @param array $objects
     *
     * @return array
     */
    public static function unsetById(&$source, $objects, $filter = true)
    {
        if (is_array($objects) && is_array($source)) {
            foreach ($source as $elemKey => $element) {
                if ($element instanceof DataObject) {
                    foreach ($objects as $key => $object) {
                        $objId = $object instanceof DataObject ? $object->id() : $object;
                        if ($objId == $element->id()) {
                            unset($source[$elemKey]);
                        }
                    }
                } elseif (is_array($element)) {
                    $source[$elemKey] = self::unsetById($element, $objects, $filter);
                }
            }
        }

        if ($filter) {
            $source = array_filter($source);
        }
        return $source;
    }


    /**
     * Filters recursive an array by elements class
     *
     * @param array|ArrayObject $objects
     * @param string $class
     *
     * @return array
     */
    public static function filterByClass(&$objects, $class)
    {
        if (is_array($objects) || $objects instanceof ArrayObject) {
            foreach ($objects as $key => $object) {
                if (!is_object($object) && is_array($object)) {
                    $objects[$key] = self::filterByClass($object, $class);
                } elseif (!is_a($object, $class)) {
                    unset($objects[$key]);
                }
            }
        }
        return $objects;
    }


    /**
     * Convert an iterator to an array.
     *
     * Converts an iterator to an array. The $recursive flag, on by default,
     * hints whether you want to do so recursively.
     *
     * @param  array|Traversable  $iterator     The array or Traversable object to convert
     * @param  bool               $recursive    Recursively check all nested structures
     * @throws InvalidArgumentException if $iterator is not an array or a Traversable object
     * @return array
     */
    public static function iteratorToArray($iterator, $recursive = true)
    {
        if (!is_array($iterator) && !$iterator instanceof Traversable) {
            throw new InvalidArgumentException(__METHOD__ . ' expects an array or Traversable object');
        }

        if (!$recursive) {
            if (is_array($iterator)) {
                return $iterator;
            }

            return iterator_to_array($iterator);
        }

        if (method_exists($iterator, 'toArray')) {
            return $iterator->toArray();
        }

        $array = array();
        foreach ($iterator as $key => $value) {
            if (is_scalar($value)) {
                $array[$key] = $value;
                continue;
            }

            if ($value instanceof Traversable) {
                $array[$key] = static::iteratorToArray($value, $recursive);
                continue;
            }

            if (is_array($value)) {
                $array[$key] = static::iteratorToArray($value, $recursive);
                continue;
            }

            $array[$key] = $value;
        }

        return $array;
    }


    /**
     * Merge two arrays together.
     *
     * If an integer key exists in both arrays, the value from the second array
     * will be appended the the first array. If both values are arrays, they
     * are merged together, else the value of the second array overwrites the
     * one of the first array.
     *
     * @param  array $a
     * @param  array $b
     * @return array
     */
    public static function merge(array $a, array $b)
    {
        foreach ($b as $key => $value) {
            if (array_key_exists($key, $a)) {
                if (is_int($key)) {
                    $a[] = $value;
                } elseif (is_array($value) && is_array($a[$key])) {
                    $a[$key] = static::merge($a[$key], $value);
                } else {
                    $a[$key] = $value;
                }
            } else {
                $a[$key] = $value;
            }
        }

        return $a;
    }


    /**
     * Counts $array dimensions
     *
     * @param array $array
     * @return int
     */
    public static function countDimensions($array)
    {
        if (!is_array($array)) {
            return 0;
        }

        if (is_array(reset($array))) {
            return self::countDimensions(reset($array)) + 1;
        } else {
            return 1;
        }
    }

    /**
     * Returns first element of array
     * @template T
     * @param array<T> $collection
     * @return T|null
     */
    public static function getFirst(array $collection)
    {
        return !empty($collection) ? reset($collection) : null;
    }

    /**
     * Sort array or array of ArrayObject (__toString),
     * user locale-sensitive, maintaining index association
     *
     * @param array $array
     * @return array
     */
    public static function sortLocale($array)
    {
        $collator = new Collator(App::$user->getLocale());
        $collator->asort($array);

        return $array;
    }

    /**
     * Given two arrays of models, $array and $related, related by the field called $fieldName
     * sort the $array by the lexicographical locale aware ordering of stringified models in $related.
     * Returns sorted array, does not mutate original input arrays.
     *
     * @param array $array
     * @param array $related
     * @param string $fieldName
     * @return array
     */
    public static function sortLocaleByRelated(array $array, array $related, string $fieldName): array
    {
        $collator = new Collator(App::$user->getLocale());
        $collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
        $collator->setAttribute(Collator::ALTERNATE_HANDLING, Collator::SHIFTED);

        usort($array, function ($a, $b) use ($collator, $related, $fieldName) {
            return $collator->compare(
                (string) $related[$a[$fieldName]],
                (string) $related[$b[$fieldName]]
            );
        });

        return $array;
    }

    /**
     * Sort the array according to the IDs passed in $items array.
     *
     * @param array<mixed> $items
     * @param array<mixed> $data
     * @return array<int, mixed>
     */
    public static function sortAsInArray(array $items, array &$data): array
    {
        $tmpData = [];
        foreach ($items as $id) {
            if (!isset($data[$id])) {
                continue;
            }

            $tmpData[$id] = $data[$id];
        }

        return $tmpData;
    }

    /**
     * @param mixed $value
     * @return array
     */
    public static function toArray($value): array
    {
        if (is_array($value)) {
            return $value;
        }

        if (is_object($value) && method_exists($value, 'toArray')) {
            return $value->toArray();
        }

        if (is_object($value) && method_exists($value, 'getArrayCopy')) {
            return $value->getArrayCopy();
        }

        if (null === $value || $value === '') {
            return [];
        }

        return [$value];
    }

    /**
     * Checks if given arrays are different. Order and repeated elements are not taken into account.
     * @param array $array1
     * @param array $array2
     * @return bool
     */
    public static function areDifferent(array $array1, array $array2): bool
    {
        return array_diff($array1, $array2) || array_diff($array2, $array1);
    }

    /**
     * @param $array
     * @param int|string $key
     * @param int|string $newItemKey
     * @param mixed $newItemValue
     * @description Allows you to add a new column (item) in the list at the selected position.
     */
    public static function insertColumn(&$array, $key, $newItemKey, $newItemValue): void
    {
        if (is_int($key)) {
            array_splice($array, $key, 0, $newItemValue);
        } else {
            $pos  = array_search($key, array_keys($array));

            $array = array_merge(
                array_slice($array, 0, $pos, true),
                [$newItemKey => $newItemValue],
                array_slice($array, $pos, null, true)
            );
        }
    }

    /**
     * @param $array
     * @return array
     * @description Flattens a multidimensional array into a single dimension array.
     */
    public static function flattenArray($array)
    {
        $result = [];

        foreach ($array as $item) {
            if (is_array($item)) {
                $result = array_merge($result, self::flattenArray($item));
            } else {
                $result[] = $item;
            }
        }

        return $result;
    }
}
