1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-tag for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-tag/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-tag/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Tag;
10
11use ArrayAccess;
12use Countable;
13use Laminas\Tag\Exception\InvalidArgumentException;
14use Laminas\Tag\Exception\OutOfBoundsException;
15use SeekableIterator;
16
17class ItemList implements Countable, SeekableIterator, ArrayAccess
18{
19    /**
20     * Items in this list
21     *
22     * @var array
23     */
24    protected $items = [];
25
26    /**
27     * Count all items
28     *
29     * @return int
30     */
31    public function count()
32    {
33        return count($this->items);
34    }
35
36    /**
37     * Spread values in the items relative to their weight
38     *
39     * @param  array $values
40     * @throws InvalidArgumentException When value list is empty
41     * @return void
42     */
43    public function spreadWeightValues(array $values)
44    {
45        // Don't allow an empty value list
46        if (count($values) === 0) {
47            throw new InvalidArgumentException('Value list may not be empty');
48        }
49
50        // Re-index the array
51        $values = array_values($values);
52
53        // If just a single value is supplied simply assign it to to all tags
54        if (count($values) === 1) {
55            foreach ($this->items as $item) {
56                $item->setParam('weightValue', $values[0]);
57            }
58        } else {
59            // Calculate min- and max-weight
60            $minWeight = null;
61            $maxWeight = null;
62
63            foreach ($this->items as $item) {
64                if ($minWeight === null && $maxWeight === null) {
65                    $minWeight = $item->getWeight();
66                    $maxWeight = $item->getWeight();
67                } else {
68                    $minWeight = min($minWeight, $item->getWeight());
69                    $maxWeight = max($maxWeight, $item->getWeight());
70                }
71            }
72
73            // Calculate the thresholds
74            $steps      = count($values);
75            $delta      = ($maxWeight - $minWeight) / ($steps - 1);
76            $thresholds = [];
77
78            for ($i = 0; $i < $steps; $i++) {
79                $thresholds[$i] = floor(100 * log(($minWeight + $i * $delta) + 2));
80            }
81
82            // Then assign the weight values
83            foreach ($this->items as $item) {
84                $threshold = floor(100 * log($item->getWeight() + 2));
85
86                for ($i = 0; $i < $steps; $i++) {
87                    if ($threshold <= $thresholds[$i]) {
88                        $item->setParam('weightValue', $values[$i]);
89                        break;
90                    }
91                }
92            }
93        }
94    }
95
96    /**
97     * Seek to an absolute position
98     *
99     * @param  int $index
100     * @throws OutOfBoundsException When the seek position is invalid
101     * @return void
102     */
103    public function seek($index)
104    {
105        $this->rewind();
106        $position = 0;
107
108        while ($position < $index && $this->valid()) {
109            $this->next();
110            $position++;
111        }
112
113        if (! $this->valid()) {
114            throw new OutOfBoundsException('Invalid seek position');
115        }
116    }
117
118    /**
119     * Return the current element
120     *
121     * @return mixed
122     */
123    public function current()
124    {
125        return current($this->items);
126    }
127
128    /**
129     * Move forward to next element
130     *
131     * @return mixed
132     */
133    public function next()
134    {
135        return next($this->items);
136    }
137
138    /**
139     * Return the key of the current element
140     *
141     * @return mixed
142     */
143    public function key()
144    {
145        return key($this->items);
146    }
147
148    /**
149     * Check if there is a current element after calls to rewind() or next()
150     *
151     * @return bool
152     */
153    public function valid()
154    {
155        return ($this->current() !== false);
156    }
157
158    /**
159     * Rewind the Iterator to the first element
160     *
161     * @return void
162     */
163    public function rewind()
164    {
165        reset($this->items);
166    }
167
168    /**
169     * Check if an offset exists
170     *
171     * @param  mixed $offset
172     * @return bool
173     */
174    public function offsetExists($offset)
175    {
176        return array_key_exists($offset, $this->items);
177    }
178
179    /**
180     * Get the value of an offset
181     *
182     * @param  mixed $offset
183     * @return TaggableInterface
184     */
185    public function offsetGet($offset)
186    {
187        return $this->items[$offset];
188    }
189
190    /**
191     * Append a new item
192     *
193     * @param  mixed          $offset
194     * @param  TaggableInterface $item
195     * @throws OutOfBoundsException When item does not implement Laminas\Tag\TaggableInterface
196     * @return void
197     */
198    public function offsetSet($offset, $item)
199    {
200        // We need to make that check here, as the method signature must be
201        // compatible with ArrayAccess::offsetSet()
202        if (! ($item instanceof TaggableInterface)) {
203            throw new OutOfBoundsException('Item must implement Laminas\Tag\TaggableInterface');
204        }
205
206        if ($offset === null) {
207            $this->items[] = $item;
208        } else {
209            $this->items[$offset] = $item;
210        }
211    }
212
213    /**
214     * Unset an item
215     *
216     * @param  mixed $offset
217     * @return void
218     */
219    public function offsetUnset($offset)
220    {
221        unset($this->items[$offset]);
222    }
223}
224