<?php
/**
 * OpenMage
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available at https://opensource.org/license/osl-3-0-php
 *
 * @category   Varien
 * @package    Varien_Data
 * @copyright  Copyright (c) 2006-2020 Magento, Inc. (https://www.magento.com)
 * @copyright  Copyright (c) 2020-2023 The OpenMage Contributors (https://www.openmage.org)
 * @license    https://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */

/**
 * Data collection
 *
 * @template T of Varien_Object
 * @implements IteratorAggregate<T>
 *
 * @category   Varien
 * @package    Varien_Data
 */
class Varien_Data_Collection implements IteratorAggregate, Countable
{
    public const SORT_ORDER_ASC = 'ASC';
    public const SORT_ORDER_DESC = 'DESC';

    /**
     * Collection items
     *
     * @var Array<T>
     */
    protected array $_items = [];

    /**
     * Item object class name
     *
     * @var class-string<T>
     */
    protected string $_itemObjectClass = Varien_Object::class;

    /**
     * Order configuration
     *
     * @var Array<string, self::SORT_ORDER_*>
     */
    protected array $_orders = [];

    /**
     * Filters configuration
     *
     * @var Array<Varien_Object>
     */
    protected array $_filters = [];

    /**
     * Filter rendered flag
     *
     * @var bool
     */
    protected bool $_isFiltersRendered = false;

    /**
     * Current page number for items pager
     */
    protected int $_curPage = 1;

    /**
     * Pager page size
     *
     * if page size is false, then we works with all items
     */
    protected int|false $_pageSize = false;

    /**
     * Total items number
     */
    protected ?int $_totalRecords = null;

    protected bool $_isCollectionLoaded = false;

    protected ?string $_cacheKey = null;

    /**
     * @var Array<string>
     */
    protected array $_cacheTags = [];

    protected int $_cacheLifetime = 86400;

    /**
     * Additional collection flags
     *
     * @var Array<string, mixed>
     */
    protected array $_flags = [];

    public function __construct()
    {
    }

    /**
     * Add collection filter
     *
     * @return $this
     */
    public function addFilter(string $field, string|array $value, string $type = 'and'): self
    {
        $filter = new Varien_Object([
            'field' => $field,
            'value' => $value,
            'type' => strtolower($type),
        ]);

        $this->_filters[] = $filter;
        $this->_isFiltersRendered = false;
        return $this;
    }

    /**
     * Search for a filter by specified field
     *
     * Multiple filters can be matched if an array is specified:
     * - 'foo' -- get the first filter with field name 'foo'
     * - array('foo') -- get all filters with field name 'foo'
     * - array('foo', 'bar') -- get all filters with field name 'foo' or 'bar'
     * - array() -- get all filters
     *
     * @template U of string|list<string>
     * @param U $field
     * @return Varien_Object|list<Varien_Object>|null
     * @psalm-return (U is list ? list<Varien_Object>|null : Varien_Object|null)
     */
    public function getFilter($field)
    {
        if (is_array($field)) {
            // empty array: get all filters
            if (empty($field)) {
                return $this->_filters;
            }
            // non-empty array: collect all filters that match specified field names
            $result = [];
            foreach ($this->_filters as $filter) {
                if (in_array($filter['field'], $field)) {
                    $result[] = $filter;
                }
            }
            return $result;
        }

        // get a first filter by specified name
        foreach ($this->_filters as $filter) {
            if ($filter['field'] === $field) {
                return $filter;
            }
        }
    }

    /**
     * Retrieve collection loading status
     */
    public function isLoaded(): bool
    {
        return $this->_isCollectionLoaded;
    }

    /**
     * Set collection loading status flag
     *
     * @return $this
     */
    protected function _setIsLoaded(bool $flag = true): self
    {
        $this->_isCollectionLoaded = $flag;
        return $this;
    }

    /**
     * Get current collection page
     */
    public function getCurPage(int $displacement = 0): int
    {
        if ($this->_curPage + $displacement <= 1) {
            return 1;
        } elseif ($this->_curPage + $displacement > $this->getLastPageNumber()) {
            return $this->getLastPageNumber();
        } else {
            return $this->_curPage + $displacement;
        }
    }

    /**
     * Retrieve collection last page number
     */
    public function getLastPageNumber(): int
    {
        $collectionSize = $this->getSize();
        if (0 === $collectionSize) {
            return 1;
        } elseif ($this->_pageSize) {
            return (int)ceil($collectionSize / $this->_pageSize);
        } else {
            return 1;
        }
    }

    /**
     * Retrieve collection page size
     *
     * @return int|false
     */
    public function getPageSize(): int|false
    {
        return $this->_pageSize;
    }

    /**
     * Retrieve collection all items count
     */
    public function getSize(): int
    {
        $this->load();
        if (is_null($this->_totalRecords)) {
            $this->_totalRecords = count($this->getItems());
        }
        return (int) $this->_totalRecords;
    }

    /**
     * Retrieve collection first item
     *
     * @return T
     */
    public function getFirstItem(): Varien_Object
    {
        $this->load();

        if (count($this->_items)) {
            reset($this->_items);
            return current($this->_items);
        }

        /**
         * @psalm-suppress UnsafeInstantiation
         */
        return new $this->_itemObjectClass();
    }

    /**
     * Retrieve collection last item
     *
     * @return T
     */
    public function getLastItem(): Varien_Object
    {
        $this->load();

        if (count($this->_items)) {
            return end($this->_items);
        }

        /**
         * @psalm-suppress UnsafeInstantiation
         */
        return new $this->_itemObjectClass();
    }

    /**
     * Retrieve collection items
     *
     * @return Array<T>
     */
    public function getItems(): array
    {
        $this->load();
        return $this->_items;
    }

    /**
     * Retrieve field values from all items
     *
     * @return list<mixed>
     */
    public function getColumnValues(string $colName): array
    {
        $this->load();

        $col = [];
        foreach ($this->getItems() as $item) {
            $col[] = $item->getData($colName);
        }
        return $col;
    }

    /**
     * Search all items by field value
     *
     * @return Array<T>
     */
    public function getItemsByColumnValue(string $column, mixed $value): array
    {
        $this->load();

        $res = [];
        foreach ($this as $item) {
            if ($item->getData($column) == $value) {
                $res[] = $item;
            }
        }
        return $res;
    }

    /**
     * Search first item by field value
     *
     * @return ?T
     */
    public function getItemByColumnValue(string $column, mixed $value): ?Varien_Object
    {
        $this->load();

        /** @var T $item */
        foreach ($this as $item) {
            if ($item->getData($column) == $value) {
                return $item;
            }
        }

        return null;
    }

    /**
     * Adding item to item array
     *
     * @param T $item
     * @return $this
     */
    public function addItem(Varien_Object $item)
    {
        $itemId = $this->_getItemId($item);

        if (!is_null($itemId)) {
            if (isset($this->_items[$itemId])) {
                throw new Exception('Item (' . get_class($item) . ') with the same id "' . $item->getId() . '" already exist');
            }
            $this->_items[$itemId] = $item;
        } else {
            $this->_addItem($item);
        }
        return $this;
    }

    /**
     * Add item that has no id to collection
     *
     * @param T $item
     * @return $this
     */
    protected function _addItem(Varien_Object $item): self
    {
        $this->_items[] = $item;
        return $this;
    }

    /**
     * Retrieve item id
     *
     * @param T $item
     */
    protected function _getItemId(Varien_Object $item): mixed
    {
        return $item->getId();
    }

    /**
     * Retrieve ids of all tems
     *
     * @return list<mixed>
     */
    public function getAllIds(): array
    {
        $ids = [];
        foreach ($this->getItems() as $item) {
            $ids[] = $this->_getItemId($item);
        }
        return $ids;
    }

    /**
     * Remove item from collection by item key
     *
     * @return $this
     */
    public function removeItemByKey(mixed $key): self
    {
        if (isset($this->_items[$key])) {
            unset($this->_items[$key]);
        }
        return $this;
    }

    /**
     * Clear collection
     *
     * @return $this
     */
    public function clear(): self
    {
        $this->_setIsLoaded(false);
        $this->_items = [];
        return $this;
    }

    /**
     * Walk through the collection and run model method or external callback
     * with optional arguments
     *
     * Returns array with results of callback for each item
     *
     * @param string|callable $callback
     * @param array $args
     * @return array
     */
    public function walk($callback, array $args = []): array
    {
        $results = [];
        $useItemCallback = is_string($callback) && !str_contains($callback, '::');
        foreach ($this->getItems() as $id => $item) {
            if ($useItemCallback) {
                $cb = [$item, $callback];
            } else {
                $cb = $callback;
                array_unshift($args, $item);
            }
            $results[$id] = call_user_func_array($cb, $args);
        }
        return $results;
    }

    /**
     * @param callable $obj_method
     * @param array $args
     */
    public function each($obj_method, $args = []): void
    {
        foreach ($args->_items as $k => $item) {
            $args->_items[$k] = call_user_func($obj_method, $item);
        }
    }

    /**
     * Setting data for all collection items
     *
     * @param string|array<string, mixed> $key
     * @param mixed $value
     * @return $this
     */
    public function setDataToAll($key, $value = null): self
    {
        if (is_array($key)) {
            foreach ($key as $k => $v) {
                $this->setDataToAll($k, $v);
            }
            return $this;
        }
        foreach ($this->getItems() as $item) {
            $item->setData($key, $value);
        }
        return $this;
    }

    /**
     * Set current page
     *
     * @return $this
     */
    public function setCurPage(int $page): self
    {
        $this->_curPage = $page;
        return $this;
    }

    /**
     * Set collection page size
     *
     * @return $this
     */
    public function setPageSize(int $size): self
    {
        $this->_pageSize = $size;
        return $this;
    }

    /**
     * Set select order
     *
     * @param self::SORT_ORDER_* $direction
     * @return $this
     */
    public function setOrder(string $field, string $direction = self::SORT_ORDER_DESC): self
    {
        $this->_orders[$field] = $direction;
        return $this;
    }

    /**
     * Set collection item class name
     *
     * @return $this
     */
    public function setItemObjectClass(string $className): self
    {
        /**
         * @var class-string<T>
         */
        $className = Mage::getConfig()->getModelClassName($className);
        /**
         * is_subclass_of($className, 'Varien_Object') - Segmentation fault in php 5.2.3
         */
        /*if (!is_subclass_of($className, 'Varien_Object')) {
            throw new Exception($className.' does not extends from Varien_Object');
        }*/
        $this->_itemObjectClass = $className;
        return $this;
    }

    /**
     * Retrieve collection empty item
     *
     * @return T
     */
    public function getNewEmptyItem(): Varien_Object
    {
        /**
         * @psalm-suppress UnsafeInstantiation
         */
        return new $this->_itemObjectClass();
    }

    /**
     * Render sql select conditions
     *
     * @return $this
     */
    protected function _renderFilters(): self
    {
        return $this;
    }

    /**
     * Render sql select orders
     *
     * @return $this
     */
    protected function _renderOrders(): self
    {
        return $this;
    }

    /**
     * Render sql select limit
     *
     * @return $this
     */
    protected function _renderLimit(): self
    {
        return $this;
    }

    /**
     * Set select distinct
     *
     * @return $this
     */
    public function distinct(bool $flag): self
    {
        return $this;
    }

    /**
     * Load data
     *
     * @return $this
     */
    public function loadData(bool $printQuery = false, bool $logQuery = false): self
    {
        return $this;
    }

    /**
     * Load data
     *
     * @return $this
     */
    public function load(bool $printQuery = false, bool $logQuery = false): self
    {
        return $this->loadData($printQuery, $logQuery);
    }

    /**
     * Convert collection to XML
     */
    public function toXml(): string
    {
        $xml = '<?xml version="1.0" encoding="UTF-8"?>
        <collection>
           <totalRecords>' . $this->_totalRecords . '</totalRecords>
           <items>';

        foreach ($this as $item) {
            $xml .= $item->toXml();
        }
        $xml .= '</items>
        </collection>';
        return $xml;
    }

    /**
     * Convert collection to array
     *
     * @param Array<string> $arrRequiredFields
     * @return array
     */
    public function toArray(array $arrRequiredFields = []): array
    {
        $arrItems = [];
        $arrItems['totalRecords'] = $this->getSize();

        $arrItems['items'] = [];
        foreach ($this as $item) {
            $arrItems['items'][] = $item->toArray($arrRequiredFields);
        }
        return $arrItems;
    }

    /**
     * Convert items array to array for select options
     *
     * return items array
     * array(
     *      $index => array(
     *          'value' => mixed
     *          'label' => mixed
     *      )
     * )
     *
     * @param string $valueField
     * @param string $labelField
     * @param array $additional
     * @return list<array{label:mixed, value:mixed}>
     */
    protected function _toOptionArray(
        string $valueField = 'id',
        string $labelField = 'name',
        $additional = []
    ): array
    {
        $data = [];
        $res = [];
        $additional['value'] = $valueField;
        $additional['label'] = $labelField;

        foreach ($this as $item) {
            foreach ($additional as $code => $field) {
                $data[$code] = $item->getData($field);
            }
            $res[] = $data;
        }
        return $res;
    }

    /**
     * @return list<array{label:mixed, value:mixed}>
     */
    public function toOptionArray(): array
    {
        return $this->_toOptionArray();
    }

    /**
     * @return Array<string, string>
     */
    public function toOptionHash(): array
    {
        return $this->_toOptionHash();
    }

    /**
     * Convert items array to hash for select options
     *
     * return items hash
     * array($value => $label)
     *
     * @return Array<string, string>
     */
    protected function _toOptionHash(string $valueField = 'id', string $labelField = 'name'): array
    {
        $res = [];
        foreach ($this as $item) {
            $res[$item->getData($valueField)] = $item->getData($labelField);
        }
        /** @var Array<string, string> */
        return $res;
    }

    /**
     * Retrieve item by id
     *
     * @return ?T
     */
    public function getItemById(mixed $idValue): ?Varien_Object
    {
        $this->load();
        if (isset($this->_items[$idValue])) {
            return $this->_items[$idValue];
        }
        return null;
    }

    public function getIterator(): Traversable
    {
        $this->load();
        return new ArrayIterator($this->_items);
    }

    /**
     * Retrieve count of collection loaded items
     */
    public function count(): int
    {
        $this->load();
        return count($this->_items);
    }

    /**
     * @return $this
     */
    public function setCacheKey(string $key): self
    {
        $this->_cacheKey = $key;
        return $this;
    }

    /**
     * @return ?string
     */
    public function getCacheKey(): ?string
    {
        return $this->_cacheKey;
    }

    /**
     * @param Array<string> $tags
     * @return $this
     */
    public function setCacheTags(array $tags): self
    {
        $this->_cacheTags = $tags;
        return $this;
    }

    /**
     * @return Array<string>
     */
    public function getCacheTags(): array
    {
        return $this->_cacheTags;
    }

    public function getCacheLifetime(): int
    {
        return $this->_cacheLifetime;
    }

    /**
     * Retrieve Flag
     *
     * @param string $flag
     * @return mixed
     */
    public function getFlag(string $flag)
    {
        return isset($this->_flags[$flag]) ? $this->_flags[$flag] : null;
    }

    /**
     * Set Flag
     *
     * @return $this
     */
    public function setFlag(string $flag, mixed $value = null): self
    {
        $this->_flags[$flag] = $value;
        return $this;
    }

    /**
     * Has Flag
     */
    public function hasFlag(string $flag): bool
    {
        return array_key_exists($flag, $this->_flags);
    }
}
