1<?php
2/*
3 * Yurii's Gantt Plugin
4 *
5 * Copyright (C) 2020 Yurii K.
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program.  If not, see http://www.gnu.org/licenses
19 */
20
21namespace dokuwiki\plugin\yuriigantt\src\Driver;
22
23use dokuwiki\plugin\yuriigantt\src\Driver\Embedded\Lexer;
24use dokuwiki\plugin\yuriigantt\src\Driver\Embedded\Handler;
25use dokuwiki\plugin\yuriigantt\src\Driver\Embedded\Renderer;
26use dokuwiki\plugin\yuriigantt\src\Entities\Link;
27use dokuwiki\plugin\yuriigantt\src\Entities\Task;
28
29class Embedded implements DriverInterface
30{
31    const DSN = ':embedded:';
32    const MODE = 'embedded';
33
34    /** @var string  */
35    protected $pageId;
36    /** @var Handler */
37    protected $handler;
38    /** @var Lexer */
39    protected $lexer;
40    /** @var bool */
41    protected $isOpen = false;
42
43
44    /**
45     * Open DB connection for the page
46     * @param string $pageId
47     * @throws \Exception
48     */
49    public function open($pageId)
50    {
51        if ($this->isOpen && $this->pageId === $pageId) {
52            return;
53        }
54
55        if ($this->isOpen && $this->pageId !== $pageId) {
56            throw new \Exception('Already open for another page! Close first!');
57        }
58
59        $this->handler = new Handler();
60        $this->lexer = new Lexer($this->handler, self::MODE);
61        Embedded::addLexerPattern($this->lexer, self::MODE);
62
63        $rawPage = rawWiki($pageId);
64        $rawPage = $rawPage === false ? false : $this->lexer->parse($rawPage);
65
66        if (!$rawPage) {
67            throw new \Exception('Failed to open dataset for page ' . $pageId);
68        }
69
70        $this->pageId = $pageId;
71        $this->isOpen = true;
72    }
73
74
75    /**
76     * Close connection
77     */
78    public function close()
79    {
80        $this->handler = $this->lexer = $this->pageId = null;
81        $this->isOpen = false;
82    }
83
84
85    protected function checkOpen()
86    {
87        if (!$this->isOpen) {
88            throw new \Exception("Database MUST BE open first!");
89        }
90    }
91
92
93    public static function emptyDatabase()
94    {
95        return <<<TXT
96~~~~GANTT~~~~
97
98~~~~~~~~~~~
99TXT;
100    }
101
102
103    public static function initDatabase($pageId)
104    {
105        $database = (object)[
106            'pageId' => $pageId,
107            'version' => '1.0',
108            'dsn' => Embedded::DSN,
109            'increment' => [
110                'task' => 1,
111                'link' => 1,
112            ],
113            'gantt' => [
114                'data' => [],
115                'links' => [],
116            ]
117        ];
118
119        if ($rawPage = io_readFile(wikiFN($pageId))) {
120            $rawPage = str_replace(self::emptyDatabase(), self::embed($database), $rawPage);
121            io_saveFile(wikiFN($pageId), $rawPage);
122        }
123
124        return $database;
125    }
126
127
128    protected function flush()
129    {
130        $this->checkOpen();
131        $renderer = new Renderer();
132
133        foreach ($this->handler->calls as $instruction) {
134            call_user_func_array([&$renderer, $instruction[0]], $instruction[1] ? $instruction[1] : []);
135        }
136
137        //file_put_contents(__DIR__ . '/test_page_output.txt', $renderer->doc);
138        io_saveFile(wikiFN($this->pageId), $renderer->doc);
139    }
140
141
142    /**
143     * Returns code ready for embedding into wiki Page
144     *
145     * @param \stdClass $database
146     * @return string
147     */
148    public static function embed(\stdClass $database)
149    {
150        $embedded = json_encode($database, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
151
152        return <<<CODE
153~~~~GANTT~~~~
154$embedded
155~~~~~~~~~~~
156CODE;
157    }
158
159    /**
160     * @param Lexer $lexer
161     * @param string $mode
162     */
163    public static function addLexerPattern($lexer, $mode)
164    {
165        $lexer->addSpecialPattern('~~~~GANTT~~~~\n.*?\n~~~~~~~~~~~', $mode, 'plugin_yuriigantt');
166    }
167
168
169    protected function getDatabase()
170    {
171        $this->checkOpen();
172        return $this->handler->getDatabase();
173    }
174
175
176    /**
177     * {@inheritdoc}
178     */
179    public function updateLink(Link $link)
180    {
181        $this->checkOpen();
182        $database =& $this->getDatabase();
183        $links =& $database->gantt->links;
184
185        for ($i = 0; $i < count($links); $i++) {
186            if ($links[$i]->id == $link->id) {
187                $links[$i] = $link;
188                break;
189            }
190        }
191
192        $this->flush();
193
194        return $link;
195    }
196
197
198    /**
199     * {@inheritdoc}
200     */
201    public function deleteLink($id)
202    {
203        $this->checkOpen();
204        $database =& $this->getDatabase();
205        $links =& $database->gantt->links;
206
207        for ($i = 0; $i < count($links); $i++) {
208            if ($links[$i]->id == $id) {
209                unset($links[$i]);
210                $links = array_values($links);
211                break;
212            }
213        }
214
215        $this->flush();
216    }
217
218
219    /**
220     * {@inheritdoc}
221     */
222    public function addLink(Link $link)
223    {
224        $this->checkOpen();
225        $database =& $this->getDatabase();
226        $link->id = $database->increment->link++;
227        $database->gantt->links[] = $link;
228
229        $this->flush();
230
231        return $link;
232    }
233
234
235    /**
236     * {@inheritdoc}
237     */
238    public function updateTask(Task $task)
239    {
240        $this->checkOpen();
241        $database =& $this->getDatabase();
242        $tasks =& $database->gantt->data;
243
244        for ($i = 0; $i < count($tasks); $i++) {
245            if ($tasks[$i]->id == $task->id) {
246                $tasks[$i] = $task;
247                break;
248            }
249        }
250
251        $this->flush();
252
253        return $task;
254    }
255
256
257    /**
258     * {@inheritdoc}
259     */
260    public function deleteTask($id)
261    {
262        $this->checkOpen();
263        $database =& $this->getDatabase();
264        $tasks =& $database->gantt->data;
265        $tasks = array_column($tasks, null, 'id'); //re-index by ID
266        $links =& $database->gantt->links;
267
268        $deleteLinks = function ($taskId) use (&$links) {
269            /** @var Link[] $links */
270            foreach ($links as &$link) {
271                if (in_array($taskId, [$link->source, $link->target])) {
272                    $link = null;
273                }
274            }
275
276            $links = array_values(array_filter($links));
277        };
278
279        $deleteChildren = function ($parentId) use (&$tasks, &$deleteChildren, $deleteLinks) {
280            foreach ($tasks as &$task) {
281                if ($task->parent == $parentId) {
282                    $deleteChildren($task->id);
283                    $deleteLinks($task->id);
284                    $task = null;
285                }
286            }
287        };
288
289        $deleteChildren($id);
290        $tasks[$id] = null;
291        $tasks = array_values(array_filter($tasks));
292        $deleteLinks($id);
293
294        $this->flush();
295    }
296
297
298    /**
299     * {@inheritdoc}
300     */
301    public function addTask(Task $task)
302    {
303        $this->checkOpen();
304        $database =& $this->getDatabase();
305        $task->id = $database->increment->task++;
306        $database->gantt->data[] = $task;
307
308        $this->flush();
309
310        return $task;
311    }
312
313}
314