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    public 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     * Adds free or busytime to the data.
50     *
51     * @param int    $start
52     * @param int    $end
53     * @param string $type  FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE
54     */
55    public function add($start, $end, $type)
56    {
57        if ($start > $this->end || $end < $this->start) {
58            // This new data is outside our timerange.
59            return;
60        }
61
62        if ($start < $this->start) {
63            // The item starts before our requested time range
64            $start = $this->start;
65        }
66        if ($end > $this->end) {
67            // The item ends after our requested time range
68            $end = $this->end;
69        }
70
71        // Finding out where we need to insert the new item.
72        $currentIndex = 0;
73        while ($start > $this->data[$currentIndex]['end']) {
74            ++$currentIndex;
75        }
76
77        // The standard insertion point will be one _after_ the first
78        // overlapping item.
79        $insertStartIndex = $currentIndex + 1;
80
81        $newItem = [
82            'start' => $start,
83            'end' => $end,
84            'type' => $type,
85        ];
86
87        $preceedingItem = $this->data[$insertStartIndex - 1];
88        if ($this->data[$insertStartIndex - 1]['start'] === $start) {
89            // The old item starts at the exact same point as the new item.
90            --$insertStartIndex;
91        }
92
93        // Now we know where to insert the item, we need to know where it
94        // starts overlapping with items on the tail end. We need to start
95        // looking one item before the insertStartIndex, because it's possible
96        // that the new item 'sits inside' the previous old item.
97        if ($insertStartIndex > 0) {
98            $currentIndex = $insertStartIndex - 1;
99        } else {
100            $currentIndex = 0;
101        }
102
103        while ($end > $this->data[$currentIndex]['end']) {
104            ++$currentIndex;
105        }
106
107        // What we are about to insert into the array
108        $newItems = [
109            $newItem,
110        ];
111
112        // This is the amount of items that are completely overwritten by the
113        // new item.
114        $itemsToDelete = $currentIndex - $insertStartIndex;
115        if ($this->data[$currentIndex]['end'] <= $end) {
116            ++$itemsToDelete;
117        }
118
119        // If itemsToDelete was -1, it means that the newly inserted item is
120        // actually sitting inside an existing one. This means we need to split
121        // the item at the current position in two and insert the new item in
122        // between.
123        if (-1 === $itemsToDelete) {
124            $itemsToDelete = 0;
125            if ($newItem['end'] < $preceedingItem['end']) {
126                $newItems[] = [
127                    'start' => $newItem['end'] + 1,
128                    'end' => $preceedingItem['end'],
129                    'type' => $preceedingItem['type'],
130                ];
131            }
132        }
133
134        array_splice(
135            $this->data,
136            $insertStartIndex,
137            $itemsToDelete,
138            $newItems
139        );
140
141        $doMerge = false;
142        $mergeOffset = $insertStartIndex;
143        $mergeItem = $newItem;
144        $mergeDelete = 1;
145
146        if (isset($this->data[$insertStartIndex - 1])) {
147            // Updating the start time of the previous item.
148            $this->data[$insertStartIndex - 1]['end'] = $start;
149
150            // If the previous and the current are of the same type, we can
151            // merge them into one item.
152            if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) {
153                $doMerge = true;
154                --$mergeOffset;
155                ++$mergeDelete;
156                $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start'];
157            }
158        }
159        if (isset($this->data[$insertStartIndex + 1])) {
160            // Updating the start time of the next item.
161            $this->data[$insertStartIndex + 1]['start'] = $end;
162
163            // If the next and the current are of the same type, we can
164            // merge them into one item.
165            if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) {
166                $doMerge = true;
167                ++$mergeDelete;
168                $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end'];
169            }
170        }
171        if ($doMerge) {
172            array_splice(
173                $this->data,
174                $mergeOffset,
175                $mergeDelete,
176                [$mergeItem]
177            );
178        }
179    }
180
181    public function getData()
182    {
183        return $this->data;
184    }
185}
186