1<?php
2
3namespace Sabre\VObject;
4
5/**
6 * FreeBusyData is a helper class that manages freebusy information.
7 *
8 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
9 * @author Evert Pot (http://evertpot.com/)
10 * @license http://sabre.io/license/ Modified BSD License
11 */
12class FreeBusyData {
13
14    /**
15     * Start timestamp
16     *
17     * @var int
18     */
19    protected $start;
20
21    /**
22     * End timestamp
23     *
24     * @var int
25     */
26    protected $end;
27
28    /**
29     * A list of free-busy times.
30     *
31     * @var array
32     */
33    protected $data;
34
35    function __construct($start, $end) {
36
37        $this->start = $start;
38        $this->end = $end;
39        $this->data = [];
40
41        $this->data[] = [
42            'start' => $this->start,
43            'end'   => $this->end,
44            'type'  => 'FREE',
45        ];
46
47    }
48
49    /**
50     * Adds free or busytime to the data.
51     *
52     * @param int $start
53     * @param int $end
54     * @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE
55     * @return void
56     */
57    function add($start, $end, $type) {
58
59        if ($start > $this->end || $end < $this->start) {
60
61            // This new data is outside our timerange.
62            return;
63
64        }
65
66        if ($start < $this->start) {
67            // The item starts before our requested time range
68            $start = $this->start;
69        }
70        if ($end > $this->end) {
71            // The item ends after our requested time range
72            $end = $this->end;
73        }
74
75        // Finding out where we need to insert the new item.
76        $currentIndex = 0;
77        while ($start > $this->data[$currentIndex]['end']) {
78            $currentIndex++;
79        }
80
81        // The standard insertion point will be one _after_ the first
82        // overlapping item.
83        $insertStartIndex = $currentIndex + 1;
84
85        $newItem = [
86            'start' => $start,
87            'end'   => $end,
88            'type'  => $type,
89        ];
90
91        $preceedingItem = $this->data[$insertStartIndex - 1];
92        if ($this->data[$insertStartIndex - 1]['start'] === $start) {
93            // The old item starts at the exact same point as the new item.
94            $insertStartIndex--;
95        }
96
97        // Now we know where to insert the item, we need to know where it
98        // starts overlapping with items on the tail end. We need to start
99        // looking one item before the insertStartIndex, because it's possible
100        // that the new item 'sits inside' the previous old item.
101        if ($insertStartIndex > 0) {
102            $currentIndex = $insertStartIndex - 1;
103        } else {
104            $currentIndex = 0;
105        }
106
107        while ($end > $this->data[$currentIndex]['end']) {
108
109            $currentIndex++;
110
111        }
112
113        // What we are about to insert into the array
114        $newItems = [
115            $newItem
116        ];
117
118        // This is the amount of items that are completely overwritten by the
119        // new item.
120        $itemsToDelete = $currentIndex - $insertStartIndex;
121        if ($this->data[$currentIndex]['end'] <= $end) $itemsToDelete++;
122
123        // If itemsToDelete was -1, it means that the newly inserted item is
124        // actually sitting inside an existing one. This means we need to split
125        // the item at the current position in two and insert the new item in
126        // between.
127        if ($itemsToDelete === -1) {
128            $itemsToDelete = 0;
129            if ($newItem['end'] < $preceedingItem['end']) {
130                $newItems[] = [
131                    'start' => $newItem['end'] + 1,
132                    'end'   => $preceedingItem['end'],
133                    'type'  => $preceedingItem['type']
134                ];
135            }
136        }
137
138        array_splice(
139            $this->data,
140            $insertStartIndex,
141            $itemsToDelete,
142            $newItems
143        );
144
145        $doMerge = false;
146        $mergeOffset = $insertStartIndex;
147        $mergeItem = $newItem;
148        $mergeDelete = 1;
149
150        if (isset($this->data[$insertStartIndex - 1])) {
151            // Updating the start time of the previous item.
152            $this->data[$insertStartIndex - 1]['end'] = $start;
153
154            // If the previous and the current are of the same type, we can
155            // merge them into one item.
156            if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) {
157                $doMerge = true;
158                $mergeOffset--;
159                $mergeDelete++;
160                $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start'];
161            }
162        }
163        if (isset($this->data[$insertStartIndex + 1])) {
164            // Updating the start time of the next item.
165            $this->data[$insertStartIndex + 1]['start'] = $end;
166
167            // If the next and the current are of the same type, we can
168            // merge them into one item.
169            if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) {
170                $doMerge = true;
171                $mergeDelete++;
172                $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end'];
173            }
174
175        }
176        if ($doMerge) {
177            array_splice(
178                $this->data,
179                $mergeOffset,
180                $mergeDelete,
181                [$mergeItem]
182            );
183        }
184
185    }
186
187    function getData() {
188
189        return $this->data;
190
191    }
192
193}
194