1<?php declare(strict_types=1);
2
3namespace DOMWrap;
4
5use DOMWrap\Traits\{
6    CommonTrait,
7    TraversalTrait,
8    ManipulationTrait
9};
10use DOMWrap\Collections\NodeCollection;
11
12/**
13 * Node List
14 *
15 * @package DOMWrap
16 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
17 */
18class NodeList extends NodeCollection
19{
20    use CommonTrait;
21    use TraversalTrait;
22    use ManipulationTrait {
23        ManipulationTrait::__call as __manipulationCall;
24    }
25
26    /** @var Document */
27    protected $document;
28
29    /**
30     * @param Document $document
31     * @param iterable $nodes
32     */
33    public function __construct(Document $document = null, iterable $nodes = null) {
34        parent::__construct($nodes);
35
36        $this->document = $document;
37    }
38
39    /**
40     * @param string $name
41     * @param array $arguments
42     *
43     * @return mixed
44     */
45    public function __call(string $name, array $arguments) {
46        try {
47            $result = $this->__manipulationCall($name, $arguments);
48        } catch (\BadMethodCallException $e) {
49            if (!$this->first() || !method_exists($this->first(), $name)) {
50                throw new \BadMethodCallException("Call to undefined method " . get_class($this) . '::' . $name . "()");
51            }
52
53            $result = call_user_func_array([$this->first(), $name], $arguments);
54        }
55
56        return $result;
57    }
58
59    /**
60     * {@inheritdoc}
61     */
62    public function collection(): NodeList {
63        return $this;
64    }
65
66    /**
67     * {@inheritdoc}
68     */
69    public function document(): ?\DOMDocument {
70        return $this->document;
71    }
72
73    /**
74     * {@inheritdoc}
75     */
76    public function result(NodeList $nodeList) {
77        return $nodeList;
78    }
79
80    /**
81     * @return NodeList
82     */
83    public function reverse(): NodeList {
84        array_reverse($this->nodes);
85
86        return $this;
87    }
88
89    /**
90     * @return mixed
91     */
92    public function first() {
93        return !empty($this->nodes) ? $this->rewind() : null;
94    }
95
96    /**
97     * @return mixed
98     */
99    public function last() {
100        return $this->end();
101    }
102
103    /**
104     * @return mixed
105     */
106    public function end() {
107        return !empty($this->nodes) ? end($this->nodes) : null;
108    }
109
110    /**
111     * @param int $key
112     *
113     * @return mixed
114     */
115    public function get(int $key) {
116        if (isset($this->nodes[$key])) {
117            return $this->nodes[$key];
118        }
119
120        return null;
121    }
122
123    /**
124     * @param int $key
125     * @param mixed $value
126     *
127     * @return self
128     */
129    public function set(int $key, $value): self {
130        $this->nodes[$key] = $value;
131
132        return $this;
133    }
134
135    /**
136     * @param callable $function
137     *
138     * @return self
139     */
140    public function each(callable $function): self {
141        foreach ($this->nodes as $index => $node) {
142            $result = $function($node, $index);
143
144            if ($result === false) {
145                break;
146            }
147        }
148
149        return $this;
150    }
151
152    /**
153     * @param callable $function
154     *
155     * @return NodeList
156     */
157    public function map(callable $function): NodeList {
158        $nodes = $this->newNodeList();
159
160        foreach ($this->nodes as $node) {
161            $result = $function($node);
162
163            if (!is_null($result) && $result !== false) {
164                $nodes[] = $result;
165            }
166        }
167
168        return $nodes;
169    }
170
171    /**
172     * @param callable $function
173     * @param mixed|null $initial
174     *
175     * @return iterable
176     */
177    public function reduce(callable $function, $initial = null) {
178        return array_reduce($this->nodes, $function, $initial);
179    }
180
181    /**
182     * @return array
183     */
184    public function toArray() {
185        return $this->nodes;
186    }
187
188    /**
189     * @param iterable $nodes
190     */
191    public function fromArray(iterable $nodes = null) {
192        $this->nodes = [];
193
194        if (is_iterable($nodes)) {
195            foreach ($nodes as $node) {
196                $this->nodes[] = $node;
197            }
198        }
199    }
200
201    /**
202     * @param NodeList|array $elements
203     *
204     * @return NodeList
205     */
206    public function merge($elements = []): NodeList {
207        if (!is_array($elements)) {
208            $elements = $elements->toArray();
209        }
210
211        return $this->newNodeList(array_merge($this->toArray(), $elements));
212    }
213
214    /**
215     * @param int $start
216     * @param int $end
217     *
218     * @return NodeList
219     */
220    public function slice(int $start, int $end = null): NodeList {
221        $nodeList = array_slice($this->toArray(), $start, $end);
222
223        return $this->newNodeList($nodeList);
224    }
225
226    /**
227     * @param \DOMNode $node
228     *
229     * @return self
230     */
231    public function push(\DOMNode $node): self {
232        $this->nodes[] = $node;
233
234        return $this;
235    }
236
237    /**
238     * @return \DOMNode
239     */
240    public function pop(): \DOMNode {
241        return array_pop($this->nodes);
242    }
243
244    /**
245     * @param \DOMNode $node
246     *
247     * @return self
248     */
249    public function unshift(\DOMNode $node): self {
250        array_unshift($this->nodes, $node);
251
252        return $this;
253    }
254
255    /**
256     * @return \DOMNode
257     */
258    public function shift(): \DOMNode {
259        return array_shift($this->nodes);
260    }
261
262    /**
263     * @param \DOMNode $node
264     *
265     * @return bool
266     */
267    public function exists(\DOMNode $node): bool {
268        return in_array($node, $this->nodes, true);
269    }
270
271    /**
272     * @param \DOMNode $node
273     *
274     * @return self
275     */
276    public function delete(\DOMNode $node): self {
277        $index = array_search($node, $this->nodes, true);
278
279        if ($index !== false) {
280            unset($this->nodes[$index]);
281        }
282
283        return $this;
284    }
285
286    /**
287     * @return bool
288     */
289    public function isRemoved(): bool {
290        return false;
291    }
292}