<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Stdlib
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */

if (version_compare(PHP_VERSION, '5.3.0', '<')) {
    /**
     * SplPriorityQueue 
     *
     * PHP 5.2.X userland implementation of PHP's SplPriorityQueue
     */
    class SplPriorityQueue implements Iterator , Countable 
    {
        /**
         * Extract data only
         */
        const EXTR_DATA = 0x00000001;

        /**
         * Extract priority only
         */
        const EXTR_PRIORITY = 0x00000002;

        /**
         * Extract an array of ('data' => $value, 'priority' => $priority)
         */
        const EXTR_BOTH = 0x00000003;

        /**
         * Count of items in the queue
         * @var int
         */
        protected $count = 0;

        /**
         * Flag indicating what should be returned when iterating or extracting
         * @var int
         */
        protected $extractFlags = self::EXTR_DATA;

        /**
         * @var bool|array
         */
        protected $preparedQueue = false;

        /**
         * All items in the queue
         * @var array
         */
        protected $queue = array();

        /**
         * Constructor
         * 
         * Creates a new, empty queue
         * 
         * @return void
         */
        public function __construct()
        {
        }

        /**
         * Compare two priorities
         *
         * Returns positive integer if $priority1 is greater than $priority2, 0 
         * if equal, negative otherwise.
         *
         * Unused internally, and only included in order to retain the same 
         * interface as PHP's SplPriorityQueue.
         *
         * @param  mixed $priority1
         * @param  mixed $priority2
         * @return int
         */
        public function compare($priority1, $priority2)
        {
            if ($priority1 > $priority2) {
                return 1;
            }
            if ($priority1 == $priority2) {
                return 0;
            }

            return -1;
        }

        /**
         * Countable: return number of items composed in the queue
         * 
         * @return int
         */
        public function count(): int
        {
            return $this->count;
        }

        /**
         * Iterator: return current item
         *
         * @return mixed
         */
        public function current(): mixed
        {
            if (!$this->preparedQueue) {
                $this->rewind();
            }
            if (!$this->count) {
                throw new OutOfBoundsException('Cannot iterate SplPriorityQueue; no elements present');
            }

if (!is_array($this->preparedQueue)) {
    throw new DomainException(sprintf(
        "Queue was prepared, but is empty?\n    PreparedQueue: %s\n    Internal Queue: %s\n",
        var_export($this->preparedQueue, 1),
        var_export($this->queue, 1)
    ));
}

            $return      = array_shift($this->preparedQueue);
            $priority    = $return['priority'];
            $priorityKey = $return['priorityKey'];
            $key         = $return['key'];
            unset($return['key']);
            unset($return['priorityKey']);
            unset($this->queue[$priorityKey][$key]);

            switch ($this->extractFlags) {
                case self::EXTR_DATA:
                    return $return['data'];
                case self::EXTR_PRIORITY:
                    return $return['priority'];
                case self::EXTR_BOTH:
                default:
                    return $return;
            };
        }

        /**
         * Extract a node from top of the heap and sift up
         *
         * Returns either the value, the priority, or both, depending on the extract flag.
         *
         * @return mixed;
         */
        public function extract()
        {
            if (!$this->count) {
                return null;
            }

            if (!$this->preparedQueue) {
                $this->prepareQueue();
            }

            if (empty($this->preparedQueue)) {
                return null;
            }

            $return      = array_shift($this->preparedQueue);
            $priority    = $return['priority'];
            $priorityKey = $return['priorityKey'];
            $key         = $return['key'];
            unset($return['key']);
            unset($return['priorityKey']);
            unset($this->queue[$priorityKey][$key]);
            $this->count--;

            switch ($this->extractFlags) {
                case self::EXTR_DATA:
                    return $return['data'];
                case self::EXTR_PRIORITY:
                    return $return['priority'];
                case self::EXTR_BOTH:
                default:
                    return $return;
            };
        }

        /**
         * Insert a value into the heap, at the specified priority
         *
         * @param  mixed $value
         * @param  mixed $priority
         * @return void
         */
        public function insert($value, $priority)
        {
            if (!is_scalar($priority)) {
                $priority = serialize($priority);
            }
            if (!isset($this->queue[$priority])) {
                $this->queue[$priority] = array();
            }
            $this->queue[$priority][] = $value;
            $this->count++;
            $this->preparedQueue = false;
        }

        /**
         * Is the queue currently empty?
         *
         * @return bool
         */
        public function isEmpty()
        {
            return (0 == $this->count);
        }

        /**
         * Iterator: return current key
         *
         * @return mixed Usually an int or string
         */
        public function key(): mixed
        {
            return $this->count;
        }

        /**
         * Iterator: Move pointer forward
         *
         * @return void
         */
        public function next()
        {
            $this->count--;
        }

        /**
         * Recover from corrupted state and allow further actions on the queue
         *
         * Unimplemented, and only included in order to retain the same interface as PHP's 
         * SplPriorityQueue.
         *
         * @return void
         */
        public function recoverFromCorruption()
        {
        }

        /**
         * Iterator: Move pointer to first item
         *
         * @return void
         */
        public function rewind(): void
        {
            if (!$this->preparedQueue) {
                $this->prepareQueue();
            }
        }

        /**
         * Set the extract flags
         * 
         * Defines what is extracted by SplPriorityQueue::current(), 
         * SplPriorityQueue::top() and SplPriorityQueue::extract().
         * 
         * - SplPriorityQueue::EXTR_DATA (0x00000001): Extract the data
         * - SplPriorityQueue::EXTR_PRIORITY (0x00000002): Extract the priority
         * - SplPriorityQueue::EXTR_BOTH (0x00000003): Extract an array containing both
         *
         * The default mode is SplPriorityQueue::EXTR_DATA.
         *
         * @param  int $flags
         * @return void
         */
        public function setExtractFlags($flags)
        {
            $expected = array(
                self::EXTR_DATA => true,
                self::EXTR_PRIORITY => true,
                self::EXTR_BOTH => true,
            );
            if (!isset($expected[$flags])) {
                throw new InvalidArgumentException(sprintf('Expected an EXTR_* flag; received %s', $flags));
            }
            $this->extractFlags = $flags;
        }

        /**
         * Return the value or priority (or both) of the top node, depending on 
         * the extract flag
         *
         * @return mixed
         */
        public function top()
        {
            $this->sort();
            $keys = array_keys($this->queue);
            $key  = array_shift($keys);
            if (preg_match('/^(a|O):/', $key)) {
                $key = unserialize($key);
            }

            if ($this->extractFlags == self::EXTR_PRIORITY) {
                return $key;
            }

            if ($this->extractFlags == self::EXTR_DATA) {
                return $this->queue[$key][0];
            }

            return array(
                'data'     => $this->queue[$key][0],
                'priority' => $key,
            );
        }

        /**
         * Iterator: is the current position valid for the queue
         *
         * @return bool
         */
        public function valid(): bool
        {
            return (bool) $this->count;
        }

        /**
         * Sort the queue
         * 
         * @return void
         */
        protected function sort()
        {
            krsort($this->queue);
        }

        /**
         * Prepare the queue for iteration and/or extraction
         * 
         * @return void
         */
        protected function prepareQueue()
        {
            $this->sort();
            $count = $this->count;
            $queue = array();
            foreach ($this->queue as $priority => $values) {
                $priorityKey = $priority;
                if (preg_match('/^(a|O):/', $priority)) {
                    $priority = unserialize($priority);
                }
                foreach ($values as $key => $value) {
                    $queue[$count] = array(
                        'data'        => $value,
                        'priority'    => $priority,
                        'priorityKey' => $priorityKey,
                        'key'         => $key,
                    );
                    $count--;
                }
            }
            $this->preparedQueue = $queue;
        }
    }
}

/**
 * Serializable version of SplPriorityQueue
 *
 * Also, provides predictable heap order for datums added with the same priority
 * (i.e., they will be emitted in the same order they are enqueued).
 *
 * @category   Zend
 * @package    Zend_Stdlib
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Stdlib_SplPriorityQueue extends SplPriorityQueue implements Serializable
{
    /**
     * @var int Seed used to ensure queue order for items of the same priority
     */
    protected $serial = PHP_INT_MAX;

    /**
     * @var bool
     */
    protected $isPhp53;

    /**
     * Constructor
     * 
     * @return void
     */
    public function __construct()
    {
        $this->isPhp53 = version_compare(PHP_VERSION, '5.3', '>=');
    }

    /**
     * Insert a value with a given priority
     *
     * Utilizes {@var $serial} to ensure that values of equal priority are 
     * emitted in the same order in which they are inserted.
     * 
     * @param  mixed $datum 
     * @param  mixed $priority 
     * @return void
     */
    public function insert($datum, $priority)
    {
        // If using the native PHP SplPriorityQueue implementation, we need to
        // hack around it to ensure that items registered at the same priority
        // return in the order registered. In the userland version, this is not
        // necessary.
        if ($this->isPhp53) {
            if (!is_array($priority)) {
                $priority = array($priority, $this->serial--);
            }
        }
        parent::insert($datum, $priority);
    }

    /**
     * Serialize to an array
     *
     * Array will be priority => data pairs
     * 
     * @return array
     */
    public function toArray()
    {
        $this->setExtractFlags(self::EXTR_BOTH);
        $array = array();
        while ($this->valid()) {
            $array[] = $this->current();
            $this->next();
        }
        $this->setExtractFlags(self::EXTR_DATA);

        // Iterating through a priority queue removes items
        foreach ($array as $item) {
            $this->insert($item['data'], $item['priority']);
        }

        // Return only the data
        $return = array();
        foreach ($array as $item) {
            $return[] = $item['data'];
        }

        return $return;
    }

    /**
     * Serialize
     * 
     * @return string
     */
    public function serialize()
    {
        return serialize($this->__serialize());
    }

    /**
     * Deserialize
     * 
     * @param  string $data
     * @return void
     */
    public function unserialize($data)
    {
        $this->__unserialize(unserialize($data));
    }

    public function __serialize(): array
    {
        $data = array();
        $this->setExtractFlags(self::EXTR_BOTH);
        while ($this->valid()) {
            $data[] = $this->current();
            $this->next();
        }
        $this->setExtractFlags(self::EXTR_DATA);

        // Iterating through a priority queue removes items
        foreach ($data as $item) {
            $this->insert($item['data'], $item['priority']);
        }

        return $data;
    }

    public function __unserialize(array $data): void
    {
        foreach ($data as $item) {
            $this->insert($item['data'], $item['priority']);
        }
    }
}
