1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5use dokuwiki\Utf8\Sort;
6
7/**
8 * Object to create a tree of values
9 *
10 * You should not create these yourself, but use the NestedResult class instead
11 */
12class NestedValue
13{
14    /** @var Value */
15    protected $value;
16
17    /** @var NestedValue[] */
18    protected $children = [];
19
20    /** @var Value[][] */
21    protected $resultRows = [];
22
23    /** @var int the nesting depth */
24    protected $depth;
25    /**
26     * @var mixed|string
27     */
28    protected $parentPath;
29
30    /**
31     * Create a nested version of the given value
32     *
33     * @param Value|null $value The value to store, null for root node
34     * @param int $depth The depth of this node (avoids collision where the same values are selected on multiple levels)
35     */
36    public function __construct(?Value $value, $parentPath = '', $depth = 0)
37    {
38        $this->value = $value;
39        $this->parentPath = $parentPath;
40        $this->depth = $depth;
41    }
42
43    /**
44     * @return int
45     */
46    public function getDepth()
47    {
48        return $this->depth;
49    }
50
51    /**
52     * @param int $depth
53     */
54    public function setDepth($depth)
55    {
56        $this->depth = $depth;
57        foreach ($this->children as $child) {
58            $child->setDepth($depth + 1);
59        }
60    }
61
62    /**
63     * Access the stored value
64     *
65     * @return Value|null the value stored in this node, null for root node
66     */
67    public function getValueObject()
68    {
69        return $this->value;
70    }
71
72    /**
73     * Add a child node
74     *
75     * Nodes with the same key (__toString()) will be overwritten
76     *
77     * @param NestedValue $child
78     * @return void
79     */
80    public function addChild(NestedValue $child)
81    {
82        $this->children[(string)$child] = $child; // ensures uniqueness
83    }
84
85    /**
86     * Get all child nodes
87     *
88     * @param bool $sort should children be sorted alphabetically?
89     * @return NestedValue[]
90     */
91    public function getChildren($sort = false)
92    {
93        $children = $this->children;
94
95        if ($sort) {
96            usort($children, [$this, 'sortChildren']);
97        } elseif (isset($children[''])) {
98            // even when not sorting, make sure the n/a entries are last
99            $naKids = $children[''];
100            unset($children['']);
101            $children[''] = $naKids;
102        }
103        return array_values($children);
104    }
105
106    /**
107     * Add a result row to this node
108     *
109     * Only unique rows will be stored, duplicates are detected by hashing the row values' toString result
110     *
111     * @param Value[] $row
112     * @return void
113     */
114    public function addResultRow($row)
115    {
116        // only add unique rows
117        $ident = md5(array_reduce($row, static fn($carry, $value) => $carry . $value, ''));
118
119        $this->resultRows[$ident] = $row;
120    }
121
122    /**
123     * Get all result rows stored in this node
124     *
125     * @return Value[][]
126     */
127    public function getResultRows()
128    {
129        return array_values($this->resultRows);
130    }
131
132    /**
133     * Get a unique key for this node
134     *
135     * @return string
136     */
137    public function __toString()
138    {
139        if (!$this->value instanceof Value) return ''; // root node
140        return $this->parentPath . '/' . $this->value->__toString();
141    }
142
143    /**
144     * Custom comparator to sort the children of this node
145     *
146     * @param NestedValue $a
147     * @param NestedValue $b
148     * @return int
149     */
150    public function sortChildren(NestedValue $a, NestedValue $b)
151    {
152        $compA = implode('-', (array)$a->getValueObject()->getCompareValue());
153        $compB = implode('-', (array)$b->getValueObject()->getCompareValue());
154
155        // sort empty values to the end
156        if ($compA === $compB) {
157            return 0;
158        }
159        if ($compA === '') {
160            return 1;
161        }
162        if ($compB === '') {
163            return -1;
164        }
165
166        // note: the way NestedResults build the NestedValues, the value object should
167        // always contain a single value only. But since the associated column is still
168        // a multi-value column, getCompareValue() will still return an array.
169        // So here we treat all returns as array and join them with a dash (even though
170        // there should never be more than one value in there)
171        return Sort::strcmp($compA, $compB);
172    }
173
174    /**
175     * print the tree for debugging
176     *
177     * @param bool $sort use sorted children?
178     * @return string
179     */
180    public function dump($sort = true)
181    {
182        $return = '';
183
184        if ($this->value) {
185            $val = implode(', ', (array)$this->value->getDisplayValue());
186            if ($val === '') $val = '{n/a}';
187            $return .= str_pad('', $this->getDepth() * 4, ' ');
188            $return .= $val;
189            $return .= "\n";
190        } else {
191            $return .= "*\n";
192        }
193
194        foreach ($this->getResultRows() as $row) {
195            $return .= str_pad('', $this->getDepth() * 4, ' ');
196            foreach ($row as $value) {
197                $val = implode(', ', (array)$value->getDisplayValue());
198                if ($val === '') $val = '{n/a}';
199                $return .= ' ' . $val;
200            }
201            $return .= "\n";
202        }
203
204        foreach ($this->getChildren($sort) as $child) {
205            $return .= $child->dump();
206        }
207
208        return $return;
209    }
210}
211