<?php

/**
 * ---------------------------------------------------------------------
 *
 * GLPI - Gestionnaire Libre de Parc Informatique
 *
 * http://glpi-project.org
 *
 * @copyright 2015-2025 Teclib' and contributors.
 * @copyright 2003-2014 by the INDEPNET Development Team.
 * @licence   https://www.gnu.org/licenses/gpl-3.0.html
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * ---------------------------------------------------------------------
 */

use Glpi\Application\ErrorHandler;
use Glpi\RichText\RichText;
use Glpi\Toolbox\Sanitizer;
use RRule\RRule;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTodo;
use Sabre\VObject\Property\FlatText;
use Sabre\VObject\Property\ICalendar\Recur;
use Sabre\VObject\Reader;

/**
 * Planning Class
 **/
class Planning extends CommonGLPI
{
    public static $rightname = 'planning';

    public static $palette_bg = ['#FFEEC4', '#D4EDFB', '#E1D0E1', '#CDD7A9', '#F8C8D2',
        '#D6CACA', '#D3D6ED', '#C8E5E3', '#FBD5BF', '#E9EBA2',
        '#E8E5E5', '#DBECDF', '#FCE7F2', '#E9D3D3', '#D2DBDC'
    ];

    public static $palette_fg = ['#57544D', '#59707E', '#5B3B5B', '#3A431A', '#58242F',
        '#3B2727', '#272D59', '#2E4645', '#6F4831', '#46481B',
        '#4E4E4E', '#274C30', '#6A535F', '#473232', '#454545',
    ];

    public static $palette_ev = ['#E94A31', '#5174F2', '#51C9F2', '#FFCC29', '#20C646',
        '#364959', '#8C5344', '#FF8100', '#F600C4', '#0017FF',
        '#000000', '#FFFFFF', '#005800', '#925EFF'
    ];

    public static $directgroup_itemtype = ['PlanningExternalEvent', 'ProjectTask', 'TicketTask', 'ProblemTask', 'ChangeTask'];

    const READMY    =    1;
    const READGROUP = 1024;
    const READALL   = 2048;

    const INFO = 0;
    const TODO = 1;
    const DONE = 2;

    /**
     * @since 0.85
     *
     * @param $nb
     **/
    public static function getTypeName($nb = 0)
    {
        return __('Planning');
    }


    public static function getMenuContent()
    {
        $menu = [];

        if (Planning::canView()) {
            $menu = [
                'title'    => static::getMenuName(),
                'shortcut' => static::getMenuShorcut(),
                'page'     => static::getSearchURL(false),
                'icon'     => static::getIcon(),
            ];

            if ($data = static::getAdditionalMenuLinks()) {
                $menu['links'] = $data;
            }

            if ($options = static::getAdditionalMenuOptions()) {
                $menu['options'] = $options;
            }
        }

        return $menu;
    }


    public static function getAdditionalMenuLinks()
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $links = [];

        if (Planning::canView()) {
            $title     = Planning::getTypeName(Session::getPluralNumber());
            $planning  = "<i class='fa far fa-calendar-alt pointer' title='$title'>
                        <span class='sr-only'>$title</span>
                       </i>";

            $links[$planning] = Planning::getSearchURL(false);
        }

        if (PlanningExternalEvent::canView()) {
            $ext_title = PlanningExternalEvent::getTypeName(Session::getPluralNumber());
            $external  = "<i class='fa fas fa-calendar-week pointer' title='$ext_title'>
                        <span class='sr-only'>$ext_title</span>
                       </i>";

            $links[$external] = PlanningExternalEvent::getSearchURL(false);
        }

        if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) {
            $caldav_title = __('CalDAV browser interface');
            $caldav  = "<i class='fa fas fa-sync pointer' title='$caldav_title'>
                        <span class='sr-only'>$caldav_title</span>
                       </i>";

            $links[$caldav] = '/caldav.php';
        }

        return $links;
    }


    public static function getAdditionalMenuOptions()
    {
        if (PlanningExternalEvent::canView()) {
            return [
                'external' => [
                    'title' => PlanningExternalEvent::getTypeName(Session::getPluralNumber()),
                    'page'  => PlanningExternalEvent::getSearchURL(false),
                    'links' => [
                        'add'    => '/front/planningexternalevent.form.php',
                        'search' => '/front/planningexternalevent.php',
                    ] + static::getAdditionalMenuLinks()
                ]
            ];
        }
        return false;
    }


    /**
     * @see CommonGLPI::getMenuShorcut()
     *
     * @since 0.85
     **/
    public static function getMenuShorcut()
    {
        return 'p';
    }


    /**
     * @since 0.85
     **/
    public static function canView()
    {

        return Session::haveRightsOr(self::$rightname, [self::READMY, self::READGROUP,
            self::READALL
        ]);
    }


    public function defineTabs($options = [])
    {

        $ong               = [];
        $ong['no_all_tab'] = true;

        $this->addStandardTab(__CLASS__, $ong, $options);

        return $ong;
    }


    public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
    {

        if ($item->getType() == __CLASS__) {
            $tabs[1] = self::getTypeName();

            return $tabs;
        }
        return '';
    }


    public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
    {

        if ($item->getType() == __CLASS__) {
            switch ($tabnum) {
                case 1: // all
                    Planning::showPlanning($_SESSION['glpiID']);
                    break;
            }
        }
        return true;
    }


    /**
     * Get planning state name
     *
     * @param $value status ID
     **/
    public static function getState($value)
    {

        switch ($value) {
            case static::INFO:
                return _n('Information', 'Information', 1);

            case static::TODO:
                return __('To do');

            case static::DONE:
                return __('Done');

            default:
                return '';
        }
    }


    /**
     * Get status icon
     *
     * @since 10.0.9
     *
     * @return string
     */
    public static function getStatusIcon($status): string
    {
        $label = htmlspecialchars(Planning::getState($status), ENT_QUOTES);
        if (empty($label)) {
            return '';
        }
        $class = Planning::getStatusClass($status);
        $color = Planning::getStatusColor($status);
        return "<i class='itilstatus $class $color me-1' title='$label' data-bs-toggle='tooltip'></i><span>" . $label . "</span>";
    }


    /**
     * Get status class
     *
     * @since 10.0.9
     *
     * @return string
     */
    public static function getStatusClass($status): string
    {
        switch ($status) {
            case Planning::INFO:
                return "ti ti-info-square-filled";

            case Planning::TODO:
                return "ti ti-alert-square-filled";

            case Planning::DONE:
                return "ti ti-square-check-filled";
        }
        return '';
    }


    /**
     * Get status color
     *
     * @since 10.0.9
     *
     * @return string
     */
    public static function getStatusColor($status): string
    {
        switch ($status) {
            case Planning::INFO:
                return "planned";

            case Planning::TODO:
                return "waiting";

            case Planning::DONE:
                return "new";
        }
        return '';
    }


    /**
     * Dropdown of planning state
     *
     * @param $name   select name
     * @param $value  default value (default '')
     * @param $display  display of send string ? (true by default)
     * @param $options  options
     **/
    public static function dropdownState($name, $value = '', $display = true, $options = [])
    {


        $js = <<<JAVASCRIPT
        templateTaskStatus = function(option) {
            if (option === false) {
                // Option is false when element does not match searched terms
                return null;
            }
            var status = option.id;
            var classes = "";
            switch (parseInt(status)) {
                case 0 :
                    classes = 'planned ti ti-info-square-filled';
                    break;
                case 1 :
                    classes = 'waiting ti ti-alert-square-filled';
                    break;
                case 2 :
                    classes = 'new ti ti-square-check-filled';
                    break;

            }
            return $('<span><i class="itilstatus ' + classes + '"></i> ' + option.text + '</span>');
        }
JAVASCRIPT;


        $p = [
            'value'             => $value,
            'showtype'          => 'normal',
            'display'           => $display,
            'templateResult'    => $js,
            'templateSelection' => $js,
        ];

        $values = [static::INFO => _n('Information', 'Information', 1),
            static::TODO => __('To do'),
            static::DONE => __('Done')
        ];

        return Dropdown::showFromArray($name, $values, array_merge($p, $options));
    }


    /**
     * Check already planned user for a period
     *
     * @param integer $users_id user id
     * @param string  $begin    begin date
     * @param string  $end      end date
     * @param array   $except   items which not be into account ['Reminder' => [1, 2, id_of_items]]
     **/
    public static function checkAlreadyPlanned($users_id, $begin, $end, $except = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $planned = false;
        $message = '';

        foreach ($CFG_GLPI['planning_types'] as $itemtype) {
            $item = new $itemtype();
            $data = $item->populatePlanning([
                'who'           => $users_id,
                'whogroup'      => 0,
                'begin'         => $begin,
                'end'           => $end,
                'check_planned' => true
            ]);
            if (isPluginItemType($itemtype)) {
                if (isset($data['items'])) {
                    $data = $data['items'];
                } else {
                    $data = [];
                }
            }

            if (
                count($data)
                && method_exists($itemtype, 'getAlreadyPlannedInformation')
            ) {
                foreach ($data as $val) {
                    if (
                        !isset($except[$itemtype])
                        || (is_array($except[$itemtype]) && !in_array($val['id'], $except[$itemtype]))
                    ) {
                         $planned  = true;
                         $message .= '- ' . $item->getAlreadyPlannedInformation($val);
                         $message .= '<br/>';
                    }
                }
            }
        }
        if ($planned) {
            $user = new User();
            $user->getFromDB($users_id);
            Session::addMessageAfterRedirect(
                sprintf(
                    __('The user %1$s is busy at the selected timeframe.'),
                    '<a href="' . $user->getFormURLWithID($users_id) . '">' . $user->getName() . '</a>'
                ) . '<br/>' . $message,
                false,
                WARNING
            );
        }
        return $planned;
    }


    /**
     * Show the availability of a user
     *
     * @since 0.83
     *
     * @param $params   array of params
     *    must contain :
     *          - begin: begin date to check (default '')
     *          - end: end date to check (default '')
     *          - itemtype : User or Object type (Ticket...)
     *          - foreign key field of the itemtype to define which item to used
     *    optional :
     *          - limitto : limit display to a specific user
     *
     * @return void
     **/
    public static function checkAvailability($params = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (!isset($params['itemtype'])) {
            return false;
        }
        if (!($item = getItemForItemtype($params['itemtype']))) {
            return false;
        }
        if (
            !isset($params[$item->getForeignKeyField()])
            || !$item->getFromDB($params[$item->getForeignKeyField()])
        ) {
            return false;
        }
       // No limit by default
        if (!isset($params['limitto'])) {
            $params['limitto'] = 0;
        }
        if (isset($params['begin']) && !empty($params['begin'])) {
            $begin = $params['begin'];
        } else {
            $begin = date("Y-m-d");
        }
        if (isset($params['end']) && !empty($params['end'])) {
            $end = $params['end'];
        } else {
            $end = date("Y-m-d");
        }

        if ($end < $begin) {
            $end = $begin;
        }
        $realbegin = $begin . " " . $CFG_GLPI["planning_begin"];
        $realend   = $end . " " . $CFG_GLPI["planning_end"];
        if ($CFG_GLPI["planning_end"] == "24:00") {
            $realend = $end . " 23:59:59";
        }

        $users = [];

        switch ($item->getType()) {
            case 'User':
                $users[$item->getID()] = $item->getName();
                break;

            default:
                if (is_a($item, 'CommonITILObject', true)) {
                    foreach ($item->getUsers(CommonITILActor::ASSIGN) as $data) {
                        $users[$data['users_id']] = getUserName($data['users_id']);
                    }
                    foreach ($item->getGroups(CommonITILActor::ASSIGN) as $data) {
                        foreach (Group_User::getGroupUsers($data['groups_id']) as $data2) {
                            $users[$data2['id']] = formatUserName(
                                $data2["id"],
                                $data2["name"],
                                $data2["realname"],
                                $data2["firstname"]
                            );
                        }
                    }
                }
                if ($itemtype = 'Ticket') {
                    $task = new TicketTask();
                } else if ($itemtype = 'Problem') {
                    $task = new ProblemTask();
                }
                if ($task->getFromDBByCrit(['tickets_id' => $item->fields['id']])) {
                    $users[$task->fields['users_id_tech']] = getUserName($task->fields['users_id_tech']);
                    $group_id = $task->fields['groups_id_tech'];
                    if ($group_id) {
                        foreach (Group_User::getGroupUsers($group_id) as $data2) {
                             $users[$data2['id']] = formatUserName(
                                 $data2["id"],
                                 $data2["name"],
                                 $data2["realname"],
                                 $data2["firstname"]
                             );
                        }
                    }
                }
                break;
        }
        asort($users);
       // Use get method to check availability
        echo "<div class='center'><form method='GET' name='form' action='planning.php'>\n";
        echo "<table class='tab_cadre_fixe'>";
        $colspan = 5;
        if (count($users) > 1) {
            $colspan++;
        }
        echo "<tr class='tab_bg_1'><th colspan='$colspan'>" . __('Availability') . "</th>";
        echo "</tr>";

        echo "<tr class='tab_bg_1'>";
        echo "<td>" . __('Start') . "</td>\n";
        echo "<td>";
        Html::showDateField("begin", ['value'      => $begin,
            'maybeempty' => false
        ]);
        echo "</td>\n";
        echo "<td>" . __('End') . "</td>\n";
        echo "<td>";
        Html::showDateField("end", ['value'      => $end,
            'maybeempty' => false
        ]);
        echo "</td>\n";
        if (count($users) > 1) {
            echo "<td width='40%'>";
            $data = [0 => __('All')];
            $data += $users;
            Dropdown::showFromArray('limitto', $data, ['width' => '100%',
                'value' => $params['limitto']
            ]);
            echo "</td>";
        }

        echo "<td class='center'>";
        echo "<input type='hidden' name='" . $item->getForeignKeyField() . "' value=\"" . $item->getID() . "\">";
        echo "<input type='hidden' name='itemtype' value=\"" . $item->getType() . "\">";
        echo "<input type='submit' class='btn btn-primary' name='checkavailability' value=\"" .
             _sx('button', 'Search') . "\">";
        echo "</td>\n";

        echo "</tr>";
        echo "</table>";
        Html::closeForm();
        echo "</div>\n";

        if (($params['limitto'] > 0) && isset($users[$params['limitto']])) {
            $displayuser[$params['limitto']] = $users[$params['limitto']];
        } else {
            $displayuser = $users;
        }

        if (count($displayuser)) {
            foreach ($displayuser as $who => $whoname) {
                $params = [
                    'who'       => $who,
                    'whogroup'  => 0,
                    'begin'     => $realbegin,
                    'end'       => $realend
                ];

                $interv = [];
                foreach ($CFG_GLPI['planning_types'] as $itemtype) {
                    $interv = array_merge($interv, $itemtype::populatePlanning($params));
                    if (method_exists($itemtype, 'populateNotPlanned')) {
                        /** @var class-string $itemtype */
                        $interv = array_merge($interv, $itemtype::populateNotPlanned($params));
                    }
                }

               // Print Headers
                echo "<br><div class='center'><table class='tab_cadre_fixe'>";
                $colnumber  = 1;
                $plan_begin = explode(":", $CFG_GLPI["planning_begin"]);
                $plan_end   = explode(":", $CFG_GLPI["planning_end"]);
                $begin_hour = intval($plan_begin[0]);
                $end_hour   = intval($plan_end[0]);
                if ($plan_end[1] != 0) {
                    $end_hour++;
                }
                $colsize    = floor((100 - 15) / ($end_hour - $begin_hour));
                $timeheader = '';
                for ($i = $begin_hour; $i < $end_hour; $i++) {
                    $from       = ($i < 10 ? '0' : '') . $i;
                    $timeheader .= "<th width='$colsize%' colspan='4'>" . $from . ":00</th>";
                    $colnumber += 4;
                }

               // Print Headers
                echo "<tr class='tab_bg_1'><th colspan='$colnumber'>";
                echo $whoname;
                echo "</th></tr>";
                echo "<tr class='tab_bg_1'><th width='15%'>&nbsp;</th>";
                echo $timeheader;
                echo "</tr>";

                $day_begin = strtotime($realbegin);
                $day_end   = strtotime($realend);

                for ($time = $day_begin; $time < $day_end; $time += DAY_TIMESTAMP) {
                    $current_day   = date('Y-m-d', $time);
                    echo "<tr><th>" . Html::convDate($current_day) . "</th>";
                    $begin_quarter = $begin_hour * 4;
                    $end_quarter   = $end_hour * 4;
                    for ($i = $begin_quarter; $i < $end_quarter; $i++) {
                        $begin_time = date("Y-m-d H:i:s", strtotime($current_day) + ($i) * HOUR_TIMESTAMP / 4);
                        $end_time   = date("Y-m-d H:i:s", strtotime($current_day) + ($i + 1) * HOUR_TIMESTAMP / 4);
                       // Init activity interval
                        $begin_act  = $end_time;
                        $end_act    = $begin_time;

                        reset($interv);
                        while ($data = current($interv)) {
                            if (
                                ($data["begin"] >= $begin_time)
                                && ($data["end"] <= $end_time)
                            ) {
                             // In
                                if ($begin_act > $data["begin"]) {
                                    $begin_act = $data["begin"];
                                }
                                if ($end_act < $data["end"]) {
                                    $end_act = $data["end"];
                                }
                                unset($interv[key($interv)]);
                            } else if (
                                ($data["begin"] < $begin_time)
                                 && ($data["end"] > $end_time)
                            ) {
                            // Through
                                $begin_act = $begin_time;
                                $end_act   = $end_time;
                                next($interv);
                            } else if (
                                ($data["begin"] >= $begin_time)
                                 && ($data["begin"] < $end_time)
                            ) {
                            // Begin
                                if ($begin_act > $data["begin"]) {
                                    $begin_act = $data["begin"];
                                }
                                $end_act = $end_time;
                                next($interv);
                            } else if (
                                ($data["end"] > $begin_time)
                                 && ($data["end"] <= $end_time)
                            ) {
                            //End
                                $begin_act = $begin_time;
                                if ($end_act < $data["end"]) {
                                    $end_act = $data["end"];
                                }
                                unset($interv[key($interv)]);
                            } else { // Defautl case
                                next($interv);
                            }
                        }
                        if ($begin_act < $end_act) {
                            if (
                                ($begin_act <= $begin_time)
                                && ($end_act >= $end_time)
                            ) {
                               // Activity in quarter
                                echo "<td class='notavailable'>&nbsp;</td>";
                            } else {
                             // Not all the quarter
                                if ($begin_act <= $begin_time) {
                                    echo "<td class='partialavailableend'>&nbsp;</td>";
                                } else {
                                    echo "<td class='partialavailablebegin'>&nbsp;</td>";
                                }
                            }
                        } else {
                           // No activity
                            echo "<td class='available'>&nbsp;</td>";
                        }
                    }
                    echo "</tr>";
                }
                echo "<tr class='tab_bg_1'><td colspan='$colnumber'>&nbsp;</td></tr>";
                echo "</table></div>";
            }
        }
        echo "<div><table class='tab_cadre'>";
        echo "<tr class='tab_bg_1'>";
        echo "<th>" . __('Caption') . "</th>";
        echo "<td class='available' colspan=8>" . __('Available') . "</td>";
        echo "<td class='notavailable' colspan=8>" . __('Unavailable') . "</td>";
        echo "</tr>";
        echo "</table></div>";
    }


    /**
     * Show the planning
     *
     * Function name change since version 0.84 show() => showPlanning
     * Function prototype changes in 9.1 (no more parameters)
     *
     * @return void
     **/
    public static function showPlanning($fullview = true)
    {
        if (!static::canView()) {
            return false;
        }

        self::initSessionForCurrentUser();

       // scheduler feature key
       // schedular part of fullcalendar is distributed with opensource licence (GLPv3)
       // but this licence is incompatible with GLPI (GPLv2)
       // see https://fullcalendar.io/license
        $scheduler_key = Plugin::doHookFunction('planning_scheduler_key');

        echo "<div" . ($fullview ? " id='planning_container'" : "") . " class='d-flex flex-wrap flex-sm-nowrap'>";

       // define options for current page
        $rand = '';
        if ($fullview) {
           // full planning view (Assistance > Planning)
            Planning::showPlanningFilter();
            $options = [
                'full_view'    => true,
                'default_view' => $_SESSION['glpi_plannings']['lastview'] ?? 'timeGridWeek',
                'license_key'  => $scheduler_key,
                'resources'    => self::getTimelineResources(),
                'now'          => date("Y-m-d H:i:s"),
                'can_create'   => PlanningExternalEvent::canCreate(),
                'can_delete'   => PlanningExternalEvent::canPurge(),
            ];
        } else {
           // short view (on Central page)
            $rand    = rand();
            $options = [
                'full_view'    => false,
                'default_view' => 'listFull',
                'header'       => false,
                'height'       => 'auto',
                'rand'         => $rand,
                'now'          => date("Y-m-d H:i:s"),
            ];
        }

       // display planning (and call js from js/planning.js)
        echo "<div id='planning$rand' class='flex-fill'></div>";
        echo "</div>";

        echo Html::scriptBlock("$(function() {
         GLPIPlanning.display(" . json_encode($options) . ");
         GLPIPlanning.planningFilters();
      });");

        return;
    }

    public static function getTimelineResources()
    {
        $resources = [];
        foreach ($_SESSION['glpi_plannings']['plannings'] as $planning_id => $planning) {
            if ($planning['type'] == 'external') {
                $resources[] = [
                    'id'         => $planning_id,
                    'title'      => $planning['name'],
                    'group_id'   => false,
                    'is_visible' => $planning['display'],
                    'itemtype'   => null,
                    'items_id'   => null
                ];
                continue; // Ignore external calendars
            }

            $exploded = explode('_', $planning_id);
            if ($planning['type'] == 'group_users') {
                $group_exploded = explode('_', $planning_id);
                $group_id = (int) $group_exploded[1];
                $group = new Group();
                $group->getFromDB($group_id);
                $resources[] = [
                    'id'         => $planning_id,
                    'title'      => $group->getName(),
                    'eventAllow' => false,
                    'is_visible' => $planning['display'],
                    'itemtype'   => 'Group_User',
                    'items_id'   => $group_id
                ];
                foreach (array_keys($planning['users']) as $planning_id_user) {
                    $child_exploded = explode('_', $planning_id_user);
                    $user = new User();
                    $users_id = (int) $child_exploded[1];
                    $user->getFromDB($users_id);
                    $planning_id_user = "gu_" . $planning_id_user;
                    $resources[] = [
                        'id'         => $planning_id_user,
                        'title'      => $user->getName(),
                        'is_visible' => $planning['display'],
                        'itemtype'   => 'User',
                        'items_id'   => $users_id,
                        'parentId'   => $planning_id,
                    ];
                }
            } else {
                $itemtype   = $exploded[0];
                $object = new $itemtype();
                $users_id = (int) $exploded[1];
                $object->getFromDB($users_id);

                $resources[] = [
                    'id'         => $planning_id,
                    'title'      => $object->getName(),
                    'group_id'   => false,
                    'is_visible' => $planning['display'],
                    'itemtype'   => $itemtype,
                    'items_id'   => $users_id
                ];
            }
        }

        return $resources;
    }

    /**
     * Return a palette array (for example self::$palette_bg)
     * @param  string $palette_name  the short name for palette (bg, fg, ev)
     * @return mixed                 the palette array or false
     *
     * @since  9.1.1
     */
    public static function getPalette($palette_name = 'bg')
    {
        if (in_array($palette_name, ['bg', 'fg', 'ev'])) {
            return self::${"palette_$palette_name"};
        }

        return false;
    }


    /**
     * Return an hexa color from a palette
     * @param  string  $palette_name the short name for palette (bg, fg, ev)
     * @param  integer $color_index  The color index in this palette
     * @return mixed                 the color in hexa (ex: #FFFFFF) or false
     *
     * @since  9.1.1
     */
    public static function getPaletteColor($palette_name = 'bg', $color_index = 0)
    {
        if ($palette = self::getPalette($palette_name)) {
            if ($color_index >= count($palette)) {
                $color_index = $color_index % count($palette);
            }

            return $palette[$color_index];
        }

        return false;
    }

    public static function getPlanningTypes()
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        return array_merge(
            $CFG_GLPI['planning_types'],
            ['NotPlanned', 'OnlyBgEvents']
        );
    }

    /**
     * Init $_SESSION['glpi_plannings'] var with thses keys :
     *  - 'filters' : type of planning available (ChangeTask, Reminder, etc)
     *  - 'plannings' : all plannings definided for current user.
     *
     * If currently logged user, has no plannings or filter, this function wiil init them
     *
     * Also manage color index in $_SESSION['glpi_plannings_color_index']
     *
     * @return void
     */
    public static function initSessionForCurrentUser()
    {
       // new user in planning, init session
        if (!isset($_SESSION['glpi_plannings']['filters'])) {
            $_SESSION['glpi_plannings']['filters']   = [];
            $_SESSION['glpi_plannings']['plannings'] = ['user_' . $_SESSION['glpiID'] => [
                'color'   => self::getPaletteColor('bg', 0),
                'display' => true,
                'type'    => 'user'
            ]
            ];
        }

       // complete missing filters
        $filters = &$_SESSION['glpi_plannings']['filters'];
        $index_color = 0;
        foreach (self::getPlanningTypes() as $planning_type) {
            if (in_array($planning_type, ['NotPlanned', 'OnlyBgEvents']) || $planning_type::canView()) {
                if (!isset($filters[$planning_type])) {
                    $filters[$planning_type] = [
                        'color'   => self::getPaletteColor('ev', $index_color),
                        'display' => !in_array($planning_type, ['NotPlanned', 'OnlyBgEvents']),
                        'type'    => 'event_filter'
                    ];
                }
                $index_color++;
            }
        }

       // compute color index for plannings
        $_SESSION['glpi_plannings_color_index'] = 0;
        foreach ($_SESSION['glpi_plannings']['plannings'] as $planning) {
            if ($planning['type'] == 'group_users') {
                $_SESSION['glpi_plannings_color_index'] += count($planning['users']);
            } else {
                $_SESSION['glpi_plannings_color_index']++;
            }
        }
    }


    /**
     * Display left part of planning who contains filters and planning with delete/toggle buttons
     * and color choosing.
     * Call self::showSingleLinePlanningFilter for each filters and plannings
     *
     * @return void
     */
    public static function showPlanningFilter()
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $headings = ['filters'    => __("Events type"),
            'plannings'  => __('Plannings')
        ];

        echo "<div id='planning_filter'>";

        echo "<div id='planning_filter_toggle'>";
        echo "<a class='toggle pointer' title='" . __s("Toggle filters") . "'></a>";
        echo "</div>";

        echo "<div id='planning_filter_content'>";
        foreach ($_SESSION['glpi_plannings'] as $filter_heading => $filters) {
            if (!in_array($filter_heading, array_keys($headings))) {
                continue;
            }

            echo "<div>";
            echo "<h3>";
            echo $headings[$filter_heading];
            if ($filter_heading == "plannings") {
                echo "<a class='planning_link planning_add_filter' href='" . $CFG_GLPI['root_doc'] .
                '/ajax/planning.php?action=add_planning_form' . "'>";
                echo "<i class='fas fa-plus-circle'></i>";
                echo "</a>";
            }
            echo "</h3>";
            echo "<ul class='filters'>";
            foreach ($filters as $filter_key => $filter_data) {
                self::showSingleLinePlanningFilter(
                    $filter_key,
                    $filter_data,
                    ['filter_color_index' => 0]
                );
            }
            echo "</ul>";
            echo "</div>";
        }
        echo "</div>"; // #planning_filter_content
        echo "</div>"; // #planning_filter
    }


    /**
     * Display a single line of planning filter.
     * See self::showPlanningFilter function
     *
     * @param $filter_key  : identify curent line of filter
     * @param $filter_data : array of filter date, must contains :
     *   * 'show_delete' (boolean): show delete button
     *   * 'filter_color_index' (integer): index of the color to use in self::$palette_bg
     * @param $options
     *
     * @return void
     */
    public static function showSingleLinePlanningFilter($filter_key, $filter_data, $options = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

       // Invalid data, skip
        if (!isset($filter_data['type'])) {
            return;
        }

        $params['show_delete']        = true;
        $params['filter_color_index'] = 0;
        if (is_array($options) && count($options)) {
            foreach ($options as $key => $val) {
                $params[$key] = $val;
            }
        }

        $actor = explode('_', $filter_key);
        $uID = 0;
        $gID = 0;
        $expanded = '';
        $title = '';
        $caldav_item_url = '';
        if ($filter_data['type'] == 'user') {
            $uID = $actor[1];
            $user = new User();
            $user_exists = $user->getFromDB($actor[1]);
            $title = $user->getName(); // Will return N/A if it doesn't exist anymore
            if ($user_exists) {
                $caldav_item_url = self::getCaldavBaseCalendarUrl($user);
            }
        } else if ($filter_data['type'] == 'group_users') {
            $group = new Group();
            $group_exists = $group->getFromDB($actor[1]);
            $title = $group->getName(); // Will return N/A if it doesn't exist anymore
            if ($group_exists) {
                $caldav_item_url = self::getCaldavBaseCalendarUrl($group);
            }
            $enabled = $disabled = 0;
            foreach ($filter_data['users'] as $user) {
                if ($user['display']) {
                    $enabled++;
                } else {
                    $disabled++;
                    $filter_data['display'] = false;
                }
            }
            if ($enabled > 0 && $disabled > 0) {
                $expanded = ' expanded';
            }
        } else if ($filter_data['type'] == 'group') {
            $gID = $actor[1];
            $group = new Group();
            $group_exists = $group->getFromDB($actor[1]);
            $title = $group->getName(); // Will return N/A if it doesn't exist anymore
            if ($group_exists) {
                $caldav_item_url = self::getCaldavBaseCalendarUrl($group);
            }
        } else if ($filter_data['type'] == 'external') {
            $title = $filter_data['name'];
        } else if ($filter_data['type'] == 'event_filter') {
            if ($filter_key == 'NotPlanned') {
                $title = __('Not planned tasks');
            } else if ($filter_key == 'OnlyBgEvents') {
                $title = __('Only background events');
            } else {
                if (!getItemForItemtype($filter_key)) {
                    return false;
                } else if (!$filter_key::canView()) {
                    return false;
                }
                $title = $filter_key::getTypeName();
            }
        }

        echo "<li event_type='" . $filter_data['type'] . "'
               event_name='$filter_key'
               class='" . $filter_data['type'] . $expanded . "'>";
        Html::showCheckbox([
            'name'          => 'filters[]',
            'value'         => $filter_key,
            'id'            => $filter_key,
            'title'         => $title,
            'checked'       => $filter_data['display']
        ]);

        if ($filter_data['type'] != 'event_filter') {
            $exploded = explode('_', $filter_data['type']);
            $icon = "user";
            if ($exploded[0] === 'group') {
                $icon = "users";
            }
            echo "<i class='actor_icon fa fa-fw fa-$icon'></i>";
        }

        echo "<label for='$filter_key'>";
        echo $title;
        $raw_url = Sanitizer::decodeHtmlSpecialChars($filter_data['url'] ?? '');
        if ($filter_data['type'] == 'external' && !Toolbox::isUrlSafe($raw_url)) {
            $warning = sprintf(__s('URL "%s" is not allowed by your administrator.'), $filter_data['url']);
            echo "<i class='fas fa-exclamation-triangle' title='{$warning}'></i>";
        }
        echo "</label>";

        $color = self::$palette_bg[$params['filter_color_index']];
        if (isset($filter_data['color']) && !empty($filter_data['color'])) {
            $color = $filter_data['color'];
        } else {
            $params['filter_color_index']++;
            $color = self::getPaletteColor('bg', $params['filter_color_index']);
        }

        echo "<span class='ms-auto d-flex align-items-center'>";
       // colors not for groups
        if ($filter_data['type'] != 'group_users' && $filter_key != 'OnlyBgEvents') {
            echo "<span class='color_input'>";
            Html::showColorField(
                $filter_key . "_color",
                ['value' => $color]
            );
            echo "</span>";
        }

        if ($filter_data['type'] == 'group_users') {
            echo "<span class='toggle pointer'></span>";
        }

        if ($filter_data['type'] != 'event_filter') {
            echo "<span class='filter_option dropstart'>";
            echo "<i class='fas fa-ellipsis-v'></i>";
            echo "<ul class='dropdown-menu '>";
            if ($params['show_delete']) {
                echo "<li class='delete_planning dropdown-item' value='$filter_key'>" . __("Delete") . "</li>";
            }
            if ($caldav_item_url !== '' && $filter_data['type'] != 'group_users' && $filter_data['type'] != 'external') {
                $url = parse_url($CFG_GLPI["url_base"]);
                $port = 80;
                if (isset($url['port'])) {
                    $port = $url['port'];
                } else if (isset($url['scheme']) && ($url["scheme"] == 'https')) {
                    $port = 443;
                }

                $loginUser = new User();
                $loginUser->getFromDB(Session::getLoginUserID(true));
                $cal_url = "/front/planning.php?genical=1&uID=" . $uID . "&gID=" . $gID .
                       //"&limititemtype=$limititemtype".
                       "&entities_id=" . $_SESSION["glpiactive_entity"] .
                       "&is_recursive=" . $_SESSION["glpiactive_entity_recursive"] .
                       "&token=" . $loginUser->getAuthToken();

                echo "<li class='dropdown-item'><a target='_blank' href='" . $CFG_GLPI["root_doc"] . "$cal_url'>" .
                 _sx("button", "Export") . " - " . __("Ical") . "</a></li>";

                echo "<li class='dropdown-item'><a target='_blank' href='webcal://" . $url['host'] . ":$port" .
                 (isset($url['path']) ? $url['path'] : '') . "$cal_url'>" .
                 _sx("button", "Export") . " - " . __("Webcal") . "</a></li>";

                echo "<li class='dropdown-item'><a target='_blank' href='" . $CFG_GLPI['root_doc'] .
                 "/front/planningcsv.php?uID=" . $uID . "&gID=" . $gID . "'>" .
                 _sx("button", "Export") . " - " . __("CSV") . "</a></li>";

                $caldav_url = $CFG_GLPI['url_base'] . '/caldav.php/' . $caldav_item_url;
                $copy_js = 'copyTextToClipboard("' . $caldav_url . '");'
                . ' alert("' . __s('CalDAV URL has been copied to clipboard') . '");'
                . ' return false;';
                echo "<li class='dropdown-item'><a target='_blank' href='#'
                 onclick='$copy_js'>" .
                 __s("Copy CalDAV URL to clipboard") . "</a></li>";
            }
            echo "</ul>";
            echo "</span>";
        }
        echo "</span>";

        if ($caldav_item_url !== '' && $filter_data['type'] == 'group_users') {
            echo "<ul class='group_listofusers filters'>";
            foreach ($filter_data['users'] as $user_key => $userdata) {
                self::showSingleLinePlanningFilter(
                    $user_key,
                    $userdata,
                    ['show_delete'        => false,
                        'filter_color_index' => $params['filter_color_index']
                    ]
                );
            }
            echo "</ul>";
        }

        echo "</li>";
    }


    /**
     * Display ajax form to add actor on planning
     *
     * @return void
     */
    public static function showAddPlanningForm()
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $rand = mt_rand();
        echo "<form action='" . self::getFormURL() . "'>";
        echo __("Actor") . ": <br>";

        $planning_types = ['user' => User::getTypeName(1)];

        if (Session::haveRightsOr('planning', [self::READGROUP, self::READALL])) {
            $planning_types['group_users'] = __('All users of a group');
            $planning_types['group']       = Group::getTypeName(1);
        }

        $planning_types['external'] = __('External calendar');

        Dropdown::showFromArray(
            'planning_type',
            $planning_types,
            ['display_emptychoice' => true,
                'rand'                =>  $rand
            ]
        );
        echo Html::scriptBlock("
      $(function() {
         $('#dropdown_planning_type$rand').on( 'change', function( e ) {
            var planning_type = $(this).val();
            $('#add_planning_subform$rand').load('" . $CFG_GLPI['root_doc'] . "/ajax/planning.php',
                                                 {action: 'add_'+planning_type+'_form'});
         });
      });");
        echo "<br><br>";
        echo "<div id='add_planning_subform$rand'></div>";
        Html::closeForm();
    }


    /**
     * Display 'User' part of self::showAddPlanningForm spcified by planning type dropdown.
     * Actually called by ajax/planning.php
     *
     * @return void
     */
    public static function showAddUserForm()
    {
        $used = [];
        foreach (array_keys($_SESSION['glpi_plannings']) as $actor) {
            $actor = explode("_", $actor);
            if ($actor[0] == "user") {
                $used[] = $actor[1];
            }
        }
        echo User::getTypeName(1) . " :<br>";

       // show only users with right to add planning events
        $rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
       // Can we see only personnal planning ?
        if (!Session::haveRightsOr('planning', [self::READALL, self::READGROUP])) {
            $rights = 'id';
        }
       // Can we see user of my groups ?
        if (
            Session::haveRight('planning', self::READGROUP)
            && !Session::haveRight('planning', self::READALL)
        ) {
            $rights = 'groups';
        }

        User::dropdown(['entity'      => $_SESSION['glpiactive_entity'],
            'entity_sons' => $_SESSION['glpiactive_entity_recursive'],
            'right'       => $rights,
            'used'        => $used
        ]);
        echo "<br /><br />";
        echo Html::hidden('action', ['value' => 'send_add_user_form']);
        echo Html::submit(_sx('button', 'Add'));
    }


    /**
     * Recieve 'User' data from self::showAddPlanningForm and save them to session and DB
     *
     * @param $params (array) : must contais form data (typically $_REQUEST)
     */
    public static function sendAddUserForm($params = [])
    {
        if (!isset($params['users_id']) || (int) $params['users_id'] <= 0) {
            Session::addMessageAfterRedirect(__('A user selection is required'), false, ERROR);
            return;
        }
        $_SESSION['glpi_plannings']['plannings']["user_" . $params['users_id']]
         = ['color'   => self::getPaletteColor('bg', $_SESSION['glpi_plannings_color_index']),
             'display' => true,
             'type'    => 'user'
         ];
        self::savePlanningsInDB();
        $_SESSION['glpi_plannings_color_index']++;
    }


    /**
     * Display 'All users of a group' part of self::showAddPlanningForm spcified by planning type dropdown.
     * Actually called by ajax/planning.php
     *
     * @return void
     */
    public static function showAddGroupUsersForm()
    {
        echo Group::getTypeName(1) . " : <br>";

        $condition = [];
       // filter groups
        if (!Session::haveRight('planning', self::READALL)) {
            $condition['id'] = $_SESSION['glpigroups'];
        }

        Group::dropdown([
            'entity'      => $_SESSION['glpiactive_entity'],
            'entity_sons' => $_SESSION['glpiactive_entity_recursive'],
            'condition'   => $condition
        ]);
        echo "<br /><br />";
        echo Html::hidden('action', ['value' => 'send_add_group_users_form']);
        echo Html::submit(_sx('button', 'Add'));
    }


    /**
     * Recieve 'All users of a group' data from self::showAddGroupUsersForm and save them to session and DB
     *
     * @since 9.1
     *
     * @param $params (array) : must contais form data (typically $_REQUEST)
     */
    public static function sendAddGroupUsersForm($params = [])
    {
        if (!isset($params['groups_id']) || (int) $params['groups_id'] <= 0) {
            Session::addMessageAfterRedirect(__('A group selection is required'), false, ERROR);
            return;
        }
        $current_group = &$_SESSION['glpi_plannings']['plannings']["group_" . $params['groups_id'] . "_users"];
        $current_group = ['display' => true,
            'type'    => 'group_users',
            'users'   => []
        ];
        $users = Group_User::getGroupUsers($params['groups_id'], [
            'glpi_users.is_active'  => 1,
            'glpi_users.is_deleted' => 0,
            [
                'OR' => [
                    ['glpi_users.begin_date' => null],
                    ['glpi_users.begin_date' => ['<', new QueryExpression('NOW()')]],
                ],
            ],
            [
                'OR' => [
                    ['glpi_users.end_date' => null],
                    ['glpi_users.end_date' => ['>', new QueryExpression('NOW()')]],
                ]
            ]
        ]);

        foreach ($users as $user_data) {
            $current_group['users']['user_' . $user_data['id']] = [
                'color'   => self::getPaletteColor('bg', $_SESSION['glpi_plannings_color_index']),
                'display' => true,
                'type'    => 'user'
            ];
            $_SESSION['glpi_plannings_color_index']++;
        }
        self::savePlanningsInDB();
    }


    public static function editEventForm($params = [])
    {
        $item = getItemForItemtype($params['itemtype']);
        if ($item instanceof CommonDBTM) {
            echo "<div class='center'>";
            echo "<a href='" . $params['url'] . "' class='btn btn-outline-secondary'>" .
                "<i class='ti ti-eye'></i>" .
                "<span>" . __("View this item in its context") . "</span>" .
            "</a>";
            echo "</div>";
            echo "<hr>";
            $rand = mt_rand();
            $options = [
                'from_planning_edit_ajax' => true,
                'formoptions'             => "id='edit_event_form$rand'",
                'start'                   => date("Y-m-d", strtotime($params['start']))
            ];
            if (isset($params['parentitemtype'])) {
                $options['parent'] = getItemForItemtype($params['parentitemtype']);
                $options['parent']->getFromDB($params['parentid']);
            }
            $item->getFromDB((int) $params['id']);
            $item->showForm((int)$params['id'], $options);
            $callback = "glpi_close_all_dialogs();
                      GLPIPlanning.refresh();
                      displayAjaxMessageAfterRedirect();";
            Html::ajaxForm("#edit_event_form$rand", $callback);
        }
    }


    /**
     * Display 'Group' part of self::showAddPlanningForm spcified by planning type dropdown.
     * Actually called by ajax/planning.php
     *
     * @since 9.1
     *
     * @return void
     */
    public static function showAddGroupForm()
    {

        $condition = ['is_task' => 1];
       // filter groups
        if (!Session::haveRight('planning', self::READALL)) {
            $condition['id'] = $_SESSION['glpigroups'];
        }

        echo Group::getTypeName(1) . " : <br>";
        Group::dropdown([
            'entity'      => $_SESSION['glpiactive_entity'],
            'entity_sons' => $_SESSION['glpiactive_entity_recursive'],
            'condition'   => $condition
        ]);
        echo "<br /><br />";
        echo Html::hidden('action', ['value' => 'send_add_group_form']);
        echo Html::submit(_sx('button', 'Add'));
    }


    /**
     * Recieve 'Group' data from self::showAddGroupForm and save them to session and DB
     *
     * @since 9.1
     *
     * @param $params (array) : must contais form data (typically $_REQUEST)
     */
    public static function sendAddGroupForm($params = [])
    {
        if (!isset($params['groups_id']) || (int) $params['groups_id'] <= 0) {
            Session::addMessageAfterRedirect(__('A group selection is required'), false, ERROR);
            return;
        }
        $_SESSION['glpi_plannings']['plannings']["group_" . $params['groups_id']]
         = ['color'   => self::getPaletteColor(
             'bg',
             $_SESSION['glpi_plannings_color_index']
         ),
             'display' => true,
             'type'    => 'group'
         ];
        self::savePlanningsInDB();
        $_SESSION['glpi_plannings_color_index']++;
    }


    /**
     * Display 'External' part of self::showAddPlanningForm specified by planning type dropdown.
     * Actually called by ajax/planning.php
     *
     * @since 9.5
     *
     * @return void
     */
    public static function showAddExternalForm()
    {

        $rand = mt_rand();

        echo '<label for ="name' . $rand . '">' . __("Calendar name") . ' : </label> ';
        echo '<br />';
        echo Html::input(
            'name',
            [
                'value' => '',
                'id'    => 'name' . $rand,
            ]
        );
        echo '<br />';
        echo '<br />';

        echo '<label for ="url' . $rand . '">' . __("Calendar URL") . ' : </label> ';
        echo '<br />';
        echo '<input type="url" name="url" id="url' . $rand . '" required>';
        echo '<br /><br />';

        echo Html::hidden('action', ['value' => 'send_add_external_form']);
        echo Html::submit(_sx('button', 'Add'));
    }


    /**
     * Receive 'External' data from self::showAddExternalForm and save them to session and DB
     *
     * @since 9.5
     *
     * @param array $params Form data
     *
     * @return void
     */
    public static function sendAddExternalForm($params = [])
    {
        if (empty($params['url'])) {
            Session::addMessageAfterRedirect(__('A url is required'), false, ERROR);
            return;
        }
        $raw_url = Sanitizer::decodeHtmlSpecialChars($params['url']);
        if (!Toolbox::isUrlSafe($raw_url)) {
            Session::addMessageAfterRedirect(
                sprintf(__('URL "%s" is not allowed by your administrator.'), $params['url']),
                false,
                ERROR
            );
            return;
        }

        $_SESSION['glpi_plannings']['plannings']['external_' . md5($params['url'])] = [
            'color'   => self::getPaletteColor('bg', $_SESSION['glpi_plannings_color_index']),
            'display' => true,
            'type'    => 'external',
            'name'    => $params['name'],
            'url'     => $params['url'],
        ];
        self::savePlanningsInDB();
        $_SESSION['glpi_plannings_color_index']++;
    }


    public static function showAddEventForm($params = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (count($CFG_GLPI['planning_add_types']) == 1) {
            $params['itemtype'] = $CFG_GLPI['planning_add_types'][0];
            self::showAddEventSubForm($params);
        } else {
            $rand = mt_rand();
            $select_options = [];
            foreach ($CFG_GLPI['planning_add_types'] as $add_types) {
                $select_options[$add_types] = $add_types::getTypeName(1);
            }
            echo __("Event type") . " : <br>";
            Dropdown::showFromArray(
                'itemtype',
                $select_options,
                ['display_emptychoice' => true,
                    'rand'                => $rand
                ]
            );

            echo Html::scriptBlock("
         $(function() {
            $('#dropdown_itemtype$rand').on('change', function() {
               var current_itemtype = $(this).val();
               $('#add_planning_subform$rand').load('" . $CFG_GLPI['root_doc'] . "/ajax/planning.php',
                                                    {action:   'add_event_sub_form',
                                                     itemtype: current_itemtype,
                                                     begin:    '" . $params['begin'] . "',
                                                     end:      '" . $params['end'] . "'});
            });
         });");
            echo "<br><br>";
            echo "<div id='add_planning_subform$rand'></div>";
        }
    }


    /**
     * Display form after selecting date range in planning
     *
     * @since 9.1
     *
     * @param $params (array): must contains this keys :
     *  - begin : start of selection range.
     *       (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
     *  - end : end of selection range.
     *       (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
     *
     * @return void
     */
    public static function showAddEventSubForm($params = [])
    {

        $rand   = mt_rand();
        $params = self::cleanDates($params);

        $params['res_itemtype'] = $params['res_itemtype'] ?? '';
        $params['res_items_id'] = $params['res_items_id'] ?? 0;
        if ($item = getItemForItemtype($params['itemtype'])) {
            $item->showForm('', [
                'from_planning_ajax' => true,
                'begin'              => $params['begin'],
                'end'                => $params['end'],
                'res_itemtype'       => $params['res_itemtype'],
                'res_items_id'       => $params['res_items_id'],
                'formoptions'        => "id='ajax_reminder$rand'"
            ]);
            $callback = "glpi_close_all_dialogs();
                      GLPIPlanning.refresh();
                      displayAjaxMessageAfterRedirect();";
            Html::ajaxForm("#ajax_reminder$rand", $callback);
        }
    }


    /**
     * Former front/planning.php before 9.1.
     * Display a classic form to plan an event (with begin fiel and duration)
     *
     * @since 9.1
     *
     * @param $params (array): array of parameters whou should contain :
     *   - id (integer): id of item who receive the planification
     *   - itemtype (string): itemtype of item who receive the planification
     *   - begin (string) : start date of event
     *   - _display_dates (bool) : display dates fields (default true)
     *   - end (optionnal) (string) : end date of event. Ifg missing, it will computerd from begin+1hour
     *   - rand_user (integer) : users_id to check planning avaibility
     *   - rand : specific rand if needed (default is generated one)
     */
    public static function showAddEventClassicForm($params = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (isset($params["id"]) && ($params["id"] > 0)) {
            echo "<input type='hidden' name='plan[id]' value='" . $params["id"] . "'>";
        }

        $rand = mt_rand();
        if (isset($params['rand'])) {
            $rand = $params['rand'];
        }

        $display_dates = $params['_display_dates'] ?? true;

        $mintime = $CFG_GLPI["planning_begin"];
        if (isset($params["begin"]) && !empty($params["begin"])) {
            $begin = $params["begin"];
            $begintime = date("H:i:s", strtotime($begin));
            if ($begintime < $mintime) {
                $mintime = $begintime;
            }
        } else {
            $ts = $CFG_GLPI['time_step'] * 60; // passage en minutes
            $time = time() + $ts - 60;
            $time = floor($time / $ts) * $ts;
            $begin = date("Y-m-d H:i", $time);
        }

        if (isset($params["end"]) && !empty($params["end"])) {
            $end = $params["end"];
        } else {
            $end = date("Y-m-d H:i:s", strtotime($begin) + HOUR_TIMESTAMP);
        }

        echo "<table class='planning_classic_card'>";

        if ($display_dates) {
            echo "<tr class='tab_bg_2'><td>" . __('Start date') . "</td><td>";
            Html::showDateTimeField("plan[begin]", [
                'value'      => $begin,
                'maybeempty' => false,
                'canedit'    => true,
                'mindate'    => '',
                'maxdate'    => '',
                'mintime'    => $mintime,
                'maxtime'    => $CFG_GLPI["planning_end"],
                'rand'       => $rand,
            ]);
            echo "</td></tr>";
        }

        echo "<tr class='tab_bg_2'><td>" . __('Period') . "&nbsp;";

        if (isset($params["rand_user"])) {
            $_POST['parent_itemtype'] = $params["parent_itemtype"] ?? '';
            $_POST['parent_items_id'] = $params["parent_items_id"] ?? '';
            $_POST['parent_fk_field'] = $params["parent_fk_field"] ?? '';
            echo "<span id='user_available" . $params["rand_user"] . "'>";
            include_once(GLPI_ROOT . '/ajax/planningcheck.php');
            echo "</span>";
        }

        echo "</td><td>";

        $empty_label   = Dropdown::EMPTY_VALUE;
        $default_delay = $params['duration'] ?? 0;
        if ($display_dates) {
            $empty_label   = __('Specify an end date');
            $default_delay = floor((strtotime($end) - strtotime($begin)) / $CFG_GLPI['time_step'] / MINUTE_TIMESTAMP) * $CFG_GLPI['time_step'] * MINUTE_TIMESTAMP;
        }

        Dropdown::showTimeStamp("plan[_duration]", [
            'min'        => 0,
            'max'        => 50 * HOUR_TIMESTAMP,
            'value'      => $default_delay,
            'emptylabel' => $empty_label,
            'rand'       => $rand,
        ]);
        echo "<br><div id='date_end$rand'></div>";

        $event_options = [
            'duration'     => '__VALUE__',
            'end'          => $end,
            'name'         => "plan[end]",
            'global_begin' => $CFG_GLPI["planning_begin"],
            'global_end'   => $CFG_GLPI["planning_end"]
        ];

        if ($display_dates) {
            Ajax::updateItemOnSelectEvent(
                "dropdown_plan[_duration]$rand",
                "date_end$rand",
                $CFG_GLPI["root_doc"] . "/ajax/planningend.php",
                $event_options
            );

            if ($default_delay == 0) {
                $params['duration'] = 0;
                Ajax::updateItem("date_end$rand", $CFG_GLPI["root_doc"] . "/ajax/planningend.php", $params);
            }
        }

        echo "</td></tr>\n";

        if (
            (!isset($params["id"]) || ($params["id"] == 0))
            && isset($params['itemtype'])
            && PlanningRecall::isAvailable()
        ) {
            echo "<tr class='tab_bg_2'><td>" . _x('Planning', 'Reminder') . "</td><td>";
            PlanningRecall::dropdown([
                'itemtype' => $params['itemtype'],
                'items_id' => $params['items_id'],
                'rand'     => $rand,
            ]);
            echo "</td></tr>";
        }
        echo "</table>\n";
    }


    /**
     * Clone an event
     *
     * @since 9.5
     *
     * @param array $event the event to clone
     *
     * @return mixed the id (integer) or false if it failed
     */
    public static function cloneEvent(array $event = [])
    {
        $item = new $event['old_itemtype']();
        $item->getFromDB((int) $event['old_items_id']);

        $input = array_merge($item->fields, [
            'plan' => [
                'begin' => date("Y-m-d H:i:s", strtotime($event['start'])),
                'end'   => date("Y-m-d H:i:s", strtotime($event['end'])),
            ],
        ]);
        unset($input['id'], $input['uuid']);

        if (isset($item->fields['name'])) {
            $input['name'] = sprintf(__('Copy of %s'), $item->fields['name']);
        }

       // manage change of assigment for CommonITILTask
        if (
            $item instanceof CommonITILTask
            && isset($event['actor']['itemtype'])
            && isset($event['actor']['items_id'])
        ) {
            switch ($event['actor']['itemtype']) {
                case "group":
                    $key = "groups_id_tech";
                    break;
                case "user":
                    $key = isset($item->fields['users_id_tech']) ? "users_id_tech" : "users_id";
                    break;
                default:
                    throw new \RuntimeException(sprintf('Unexpected event actor itemtype `%s`.', $event['actor']['itemtype']));
                    break;
            }

            unset(
                $input['users_id_tech'],
                $input['users_id'],
                $input['groups_id_tech'],
                $input['groups_id']
            );

            $input[$key] = $event['actor']['items_id'];
        }

        $new_items_id = $item->add(Toolbox::addslashes_deep($input));

       // manage all assigments for ProjectTask
        if (
            $item instanceof ProjectTask
            && isset($event['actor']['itemtype'])
            && isset($event['actor']['items_id'])
        ) {
            $team = new ProjectTaskTeam();
            $team->add([
                'projecttasks_id' => $new_items_id,
                'itemtype'        => ucfirst($event['actor']['itemtype']),
                'items_id'        => $event['actor']['items_id']
            ]);
        }

        return $new_items_id;
    }

    /**
     * Delete an event
     *
     * @since 9.5
     *
     * @param array $event the event to clone (with itemtype and items_id keys)
     *
     * @return bool
     */
    public static function deleteEvent(array $event = []): bool
    {
        $item = new $event['itemtype']();

        if (
            isset($event['day'])
            && isset($event['instance'])
            && $event['instance']
            && method_exists($item, "deleteInstance")
        ) {
            return $item->deleteInstance((int) $event['items_id'], $event['day']);
        } else {
            return $item->delete([
                'id' => (int) $event['items_id']
            ]);
        }
    }


    /**
     * toggle display for selected line of $_SESSION['glpi_plannings']
     *
     * @since 9.1
     *
     * @param  array $options: should contains :
     *  - type : event type, can be event_filter, user, group or group_users
     *  - parent : in case of type=users_group, must contains the id of the group
     *  - name : contains a string with type and id concatened with a '_' char (ex user_41).
     *  - display : boolean value to set to his line
     * @return void
     */
    public static function toggleFilter($options = [])
    {

        $key = 'filters';
        if (in_array($options['type'], ['user', 'group', 'group_users', 'external'])) {
            $key = 'plannings';
        }
        if (
            !isset($options['parent'])
            || empty($options['parent'])
        ) {
            $_SESSION['glpi_plannings'][$key][$options['name']]['display']
            = ($options['display'] === 'true');
        } else {
            $_SESSION['glpi_plannings']['plannings'][$options['parent']]['users']
            [$options['name']]['display']
            = ($options['display'] === 'true');
        }
        self::savePlanningsInDB();
    }


    /**
     * change color for selected line of $_SESSION['glpi_plannings']
     *
     * @since 9.1
     *
     * @param  array $options: should contains :
     *  - type : event type, can be event_filter, user, group or group_users
     *  - parent : in case of type=users_group, must contains the id of the group
     *  - name : contains a string with type and id concatened with a '_' char (ex user_41).
     *  - color : rgb color (preceded by '#'' char)
     * @return void
     */
    public static function colorFilter($options = [])
    {
        $key = 'filters';
        if (in_array($options['type'], ['user', 'group', 'group_users', 'external'])) {
            $key = 'plannings';
        }
        if (
            !isset($options['parent'])
            || empty($options['parent'])
        ) {
            $_SESSION['glpi_plannings'][$key][$options['name']]['color'] = $options['color'];
        } else {
            $_SESSION['glpi_plannings']['plannings'][$options['parent']]['users']
            [$options['name']]['color'] = $options['color'];
        }
        self::savePlanningsInDB();
    }


    /**
     * delete selected line in $_SESSION['glpi_plannings']
     *
     * @since 9.1
     *
     * @param  array $options: should contains :
     *  - type : event type, can be event_filter, user, group or group_users
     *  - filter : contains a string with type and id concatened with a '_' char (ex user_41).
     * @return void
     */
    public static function deleteFilter($options = [])
    {

        $current = $_SESSION['glpi_plannings']['plannings'][$options['filter']];
        if ($current['type'] == 'group_users') {
            $_SESSION['glpi_plannings_color_index'] -= count($current['users']);
        } else {
            $_SESSION['glpi_plannings_color_index']--;
        }

        unset($_SESSION['glpi_plannings']['plannings'][$options['filter']]);
        self::savePlanningsInDB();
    }


    public static function savePlanningsInDB()
    {

        $user = new User();
        $user->update(['id' => $_SESSION['glpiID'],
            'plannings' => exportArrayToDB($_SESSION['glpi_plannings'])
        ]);
    }


    /**
     * Prepare a set of events for jquery fullcalendar.
     * Call populatePlanning functions for all $CFG_GLPI['planning_types'] types
     *
     * @since 9.1
     *
     * @param array $options with this keys:
     *  - begin: mandatory, planning start.
     *       (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
     *  - end: mandatory, planning end.
     *       (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
     *  - display_done_events: default true, show also events tagged as done
     *  - force_all_events: even if the range is big, don't reduce the returned set
     * @return array $events : array with events in fullcalendar.io format
     */
    public static function constructEventsArray($options = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $param['start']               = '';
        $param['end']                 = '';
        $param['view_name']           = '';
        $param['display_done_events'] = true;
        $param['force_all_events']    = false;

        if (is_array($options) && count($options)) {
            foreach ($options as $key => $val) {
                $param[$key] = $val;
            }
        }

        $timezone = new DateTimeZone(date_default_timezone_get());
        $time_begin = strtotime($param['start']) - $timezone->getOffset(new DateTime($param['start']));
        $time_end   = strtotime($param['end']) - $timezone->getOffset(new DateTime($param['end']));

       // if the dates range is greater than a certain amount, and we're not on a list view
       // we certainly are on this view (as our biggest view apart list is month one).
       // we must avoid at all cost to calculate rrules events on a big range
        if (
            !$param['force_all_events']
            && $param['view_name'] != "listFull"
            && ($time_end - $time_begin) > (2 * MONTH_TIMESTAMP)
        ) {
            $param['view_name'] = "listFull";
            return [];
        }

        $param['begin'] = date("Y-m-d H:i:s", $time_begin);
        $param['end']   = date("Y-m-d H:i:s", $time_end);

        $raw_events = [];
        $not_planned = [];
        foreach ($CFG_GLPI['planning_types'] as $planning_type) {
            if (!$planning_type::canView()) {
                continue;
            }
            if ($_SESSION['glpi_plannings']['filters'][$planning_type]['display']) {
                $event_type_color = $_SESSION['glpi_plannings']['filters'][$planning_type]['color'];
                foreach ($_SESSION['glpi_plannings']['plannings'] as $actor => $actor_params) {
                    if ($actor_params['type'] == 'external') {
                        continue; // Ignore external calendars
                    }
                    $actor_params['event_type_color'] = $event_type_color;
                    $actor_params['planning_type'] = $planning_type;
                    self::constructEventsArraySingleLine(
                        $actor,
                        array_merge($param, $actor_params),
                        $raw_events,
                        $not_planned
                    );
                }
            }
        }

       //handle not planned events
        $raw_events = array_merge($raw_events, $not_planned);

       // get external calendars events (ical)
       // and on list view, only get future events
        $begin_ical = $param['begin'];
        if ($param['view_name'] == "listFull") {
            $begin_ical = date('Y-m-d 00:00:00');
        }
        $raw_events = array_merge(
            $raw_events,
            self::getExternalCalendarRawEvents($begin_ical, $param['end'])
        );

       // construct events (in fullcalendar format)
        $events = [];
        foreach ($raw_events as $event) {
            if (
                $_SESSION['glpi_plannings']['filters']['OnlyBgEvents']['display']
                && (!isset($event['background']) || !$event['background'])
            ) {
                continue;
            }

            $users_id = (isset($event['users_id_tech']) && !empty($event['users_id_tech']) ?
                        $event['users_id_tech'] :
                        $event['users_id']);
            $content = Planning::displayPlanningItem($event, $users_id, 'in', false) ?: ($event['content'] ?? "");
            $tooltip = Planning::displayPlanningItem($event, $users_id, 'in', true) ?: ($event['tooltip'] ?? "");

           // dates should be set with the user timezone
            $begin = $event['begin'];
            $end   = $event['end'];

           // retreive all day events
            if (
                strpos($event['begin'], "00:00:00") != false
                && (strtotime($event['end']) - strtotime($event['begin'])) % DAY_TIMESTAMP == 0
            ) {
                 $begin = date('Y-m-d', strtotime($event['begin']));
                 $end = date('Y-m-d', strtotime($event['end']));
            }

           // get duration in milliseconds
            $ms_duration = (strtotime($end) - strtotime($begin)) * 1000;

            $index_color = array_search("user_$users_id", array_keys($_SESSION['glpi_plannings']));
            $new_event = [
                'title'       => $event['name'],
                'content'     => $content,
                'tooltip'     => $tooltip,
                'start'       => $begin,
                'end'         => $end,
                'duration'    => $ms_duration,
                '_duration'   => $ms_duration, // sometimes duration is removed from event object in fullcalendar
                '_editable'   => $event['editable'], // same, avoid loss of editable key in fullcalendar
                'rendering'   => isset($event['background'])
                             && $event['background']
                             && !$_SESSION['glpi_plannings']['filters']['OnlyBgEvents']['display']
                              ? 'background'
                              : '',
                'color'       => (empty($event['color']) ?
                              Planning::$palette_bg[$index_color] :
                              $event['color']),
                'borderColor' => (empty($event['event_type_color']) ?
                              self::getPaletteColor('ev', $event['itemtype']) :
                              $event['event_type_color']),
                'textColor'   => Planning::$palette_fg[$index_color],
                'typeColor'   => (empty($event['event_type_color']) ?
                              self::getPaletteColor('ev', $event['itemtype']) :
                              $event['event_type_color']),
                'url'         => $event['url'] ?? "",
                'ajaxurl'     => $event['ajaxurl'] ?? "",
                'itemtype'    => $event['itemtype'] ?? "",
                'parentitemtype' => $event['parentitemtype'] ?? "",
                'items_id'    => $event['id'] ?? "",
                'resourceId'  => $event['resourceId'] ?? "",
                'priority'    => $event['priority'] ?? "",
                'state'       => $event['state'] ?? "",
            ];

            // if duration is full day and start is midnight, force allDay to true
            if (date('H:i:s', strtotime($begin)) === '00:00:00' && (int) $ms_duration % (DAY_TIMESTAMP * 1000) === 0) {
                $new_event['allDay'] = true;
            }

           // if we can't update the event, pass the editable key
            if (!$event['editable']) {
                $new_event['editable'] = false;
            }

           // override color if view is ressource and category color exists
           // maybe we need a better way for displaying categories color
            if (
                $param['view_name'] == "resourceWeek"
                && !empty($event['event_cat_color'])
            ) {
                $new_event['color'] = $event['event_cat_color'];
            }

           // manage reccurent events
            if (isset($event['rrule']) && count($event['rrule'])) {
                $rrule = $event['rrule'];

               // the fullcalencard plugin waits for integer types for number (not strings)
                if (isset($rrule['interval'])) {
                    $rrule['interval'] = (int) $rrule['interval'];
                }
                if (isset($rrule['count'])) {
                    $rrule['count'] = (int) $rrule['count'];
                }

               // clean empty values in rrule
                foreach ($rrule as $key => $value) {
                    if (is_null($value) || $value == '') {
                        unset($rrule[$key]);
                    }
                }

                $rset = PlanningExternalEvent::getRsetFromRRuleField($rrule, $new_event['start']);

               // append icon to distinguish reccurent event in views
               // use UTC datetime to avoid some issues with rlan/phprrule
                $dtstart_datetime  = new \DateTime($new_event['start']);
                unset($rrule['exceptions']); // remove exceptions key (as libraries throw exception for unknow keys)
                $hr_rrule_o = new RRule(
                    array_merge(
                        $rrule,
                        [
                            'dtstart' => $dtstart_datetime->format('Ymd\THis\Z')
                        ]
                    )
                );
                $new_event = array_merge($new_event, [
                    'icon'     => 'fas fa-history',
                    'icon_alt' => $hr_rrule_o->humanReadable(),
                ]);

               // for fullcalendar, we need to pass start in the rrule key
                unset($new_event['start'], $new_event['end']);

               // For list view, only display only the next occurence
               // to avoid issues performances (range in list view can be 10 years long)
                if ($param['view_name'] == "listFull") {
                    /** @var DateTime $next_date */
                    $next_date = $rset->getNthOccurrenceAfter(new DateTime(), 1);
                    if ($next_date) {
                        $new_event = array_merge($new_event, [
                            'start'    => $next_date->format('c'),
                            'end'      => $next_date->add(new DateInterval("PT" . ($ms_duration / 1000) . "S"))
                                            ->format('c'),
                        ]);
                    }
                } else {
                    $rrule_string = "";
                    foreach ($rset->getRRules() as $occurence) {
                        $rrule_string .= $occurence->rfcString(false) . "\n";
                    }
                    $ex_dates = [];
                    foreach ($rset->getExDates() as $occurence) {
                       // we forge the ex date with only the date part of the exception
                       // and the hour of the dtstart.
                       // This to presents only date selection to the user
                        $ex_dates[] = "EXDATE:" . $occurence->format('Ymd\THis');
                    }

                    if (count($ex_dates)) {
                        $rrule_string .= implode("\n", $ex_dates) . "\n";
                    }

                    $new_event = array_merge($new_event, [
                        'is_recurrent' => true,
                        'rrule'        => $rrule_string,
                        'duration'     => $ms_duration
                    ]);
                }
            }

            $events[] = $new_event;
        }

        return $events;
    }


    /**
     * construct a single line for self::constructEventsArray()
     * Recursively called to construct $raw_events param.
     *
     * @since 9.1
     *
     * @param string $actor: a type and id concaneted separated by '_' char, ex 'user_41'
     * @param array  $params: must contains this keys :
     *  - display: boolean for pass or not the consstruction of this line (a group of users can be displayed but its users not).
     *  - type: event type, can be event_filter, user, group or group_users
     *  - who: integer for identify user
     *  - whogroup: integer for identify group
     *  - color: string with #rgb color for event's foreground color.
     *  - event_type_color : string with #rgb color for event's foreground color.
     * @param array  $raw_events: (passed by reference) the events array in construction
     * @param array  $not_planned (passed by references) not planned events array in construction
     * @return void
     */
    public static function constructEventsArraySingleLine($actor, $params = [], &$raw_events = [], &$not_planned = [])
    {

        if ($params['display']) {
            $actor_array = explode("_", $actor);
            if ($params['type'] == "group_users") {
                $subparams = $params;
                unset($subparams['users']);
                $subparams['from_group_users'] = true;
                foreach ($params['users'] as $user => $userdata) {
                    $subparams = array_merge($subparams, $userdata);
                    self::constructEventsArraySingleLine($user, $subparams, $raw_events, $not_planned);
                }
            } else {
                $params['who']       = $actor_array[1];
                $params['whogroup']  = 0;
                if (
                    $params['type'] == "group"
                    && in_array($params['planning_type'], self::$directgroup_itemtype)
                ) {
                    $params['who']       = 0;
                    $params['whogroup']  = $actor_array[1];
                }

                $current_events = $params['planning_type']::populatePlanning($params);
                if (count($current_events) > 0) {
                    $raw_events = array_merge($raw_events, $current_events);
                }
                if (
                    $_SESSION['glpi_plannings']['filters']['NotPlanned']['display']
                    && method_exists($params['planning_type'], 'populateNotPlanned')
                ) {
                    /** @var class-string $params['planning_type'] */
                    $not_planned = array_merge($not_planned, $params['planning_type']::populateNotPlanned($params));
                }
            }
        }

        if (isset($params['from_group_users']) && $params['from_group_users']) {
            $actor = "gu_" . $actor;
        }

       // fill type of planning
        $raw_events = array_map(function ($arr) use ($actor) {
            return $arr + ['resourceId' => $actor];
        }, $raw_events);

        if ($_SESSION['glpi_plannings']['filters']['NotPlanned']['display']) {
            $not_planned = array_map(function ($arr) use ($actor) {
                return $arr + [
                    'not_planned' => true,
                    'resourceId' => $actor,
                    'event_type_color' => $_SESSION['glpi_plannings']['filters']['NotPlanned']['color']
                ];
            }, $not_planned);
        }
    }

    /**
     * Return events fetched from user external calendars.
     *
     * @return array
     */
    private static function getExternalCalendarRawEvents(string $limit_begin, string $limit_end): array
    {
        ErrorHandler::getInstance()->suspendOutput(); // Suspend error output to prevent warnings to corrupr JSON output

        $raw_events = [];

        foreach ($_SESSION['glpi_plannings']['plannings'] as $planning_id => $planning_params) {
            if ('external' !== $planning_params['type'] || !$planning_params['display']) {
                continue; // Ignore non external and inactive calendars
            }
            $raw_url = Sanitizer::decodeHtmlSpecialChars($planning_params['url']);
            $calendar_data = Toolbox::getURLContent($raw_url);
            if (empty($calendar_data)) {
                continue;
            }
            try {
                $vcalendar = Reader::read($calendar_data);
            } catch (\Sabre\VObject\ParseException $exception) {
                trigger_error(
                    sprintf('Unable to parse calendar data from URL "%s"', $raw_url),
                    E_USER_WARNING
                );
                continue;
            }
            if (!$vcalendar instanceof VCalendar) {
                trigger_error(
                    sprintf('No VCalendar object found at URL "%s"', $raw_url),
                    E_USER_WARNING
                );
                continue;
            }
            foreach ($vcalendar->getComponents() as $vcomp) {
                if (!($vcomp instanceof VEvent || $vcomp instanceof VTodo)) {
                    continue;
                }

                $end_date_prop = $vcomp instanceof VTodo ? 'DUE' : 'DTEND';
                if (
                    !$vcomp->DTSTART instanceof \Sabre\VObject\Property\ICalendar\DateTime
                    || !$vcomp->$end_date_prop instanceof \Sabre\VObject\Property\ICalendar\DateTime
                ) {
                    continue;
                }
                $user_tz  = new \DateTimeZone(date_default_timezone_get());
                $begin_dt = $vcomp->DTSTART->getDateTime();
                $begin_dt = $begin_dt->setTimeZone($user_tz);
                $end_dt   = $vcomp->$end_date_prop->getDateTime();
                $end_dt   = $end_dt->setTimeZone($user_tz);

                if (
                    !($vcomp->RRULE instanceof Recur)
                    && ($limit_end < $begin_dt->format('Y-m-d H:i:s') || $limit_begin > $end_dt->format('Y-m-d H:i:s'))
                ) {
                    continue; // Ignore events not inside dates range
                }

                $title = $vcomp->SUMMARY instanceof FlatText ? $vcomp->SUMMARY->getValue() : '';
                $description = $vcomp->DESCRIPTION instanceof FlatText ? $vcomp->DESCRIPTION->getValue() : '';

                $raw_events[] = [
                    'users_id'         => Session::getLoginUserID(),
                    'name'             => $title,
                    'tooltip'          => trim($title . "\n" . $description),
                    'content'          => $description,
                    'begin'            => $begin_dt->format('Y-m-d H:i:s'),
                    'end'              => $end_dt->format('Y-m-d H:i:s'),
                    'event_type_color' => $planning_params['color'],
                    'color'            => $planning_params['color'],
                    'rrule'            => $vcomp->RRULE instanceof Recur
                  ? current($vcomp->RRULE->getJsonValue())
                  : null,
                    'editable'         => false,
                    'resourceId'       => $planning_id,
                ];
            }
        }

        ErrorHandler::getInstance()->unsuspendOutput(); // Restore error output state

        return $raw_events;
    }


    /**
     * Change dates of a selected event.
     * Called from a drag&drop in planning
     *
     * @since 9.1
     *
     * @param array $params must contains this keys :
     *  - items_id : integer to identify items
     *  - itemtype : string to identify items
     *  - start : planning start .
     *       (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
     *  - end : planning end .
     *       (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
     * @return bool
     */
    public static function updateEventTimes($params = [])
    {
        if ($item = getItemForItemtype($params['itemtype'])) {
            $params = self::cleanDates($params);

            if (
                $item->getFromDB($params['items_id'])
                && empty($item->fields['is_deleted'])
                && $item::canUpdate()
                && $item->canUpdateItem()
            ) {
                // item exists and is not in bin

                $abort = false;

                // if event has rrule property, check if we need to create a clone instance
                if (
                    isset($item->fields['rrule'])
                    && strlen($item->fields['rrule'])
                ) {
                    if (
                        isset($params['move_instance'])
                        && filter_var($params['move_instance'], FILTER_VALIDATE_BOOLEAN)
                        && method_exists($item, 'createInstanceClone')
                    ) {
                        $item = $item->createInstanceClone(
                            $item->fields['id'],
                            $params['old_start']
                        );
                            $params['items_id'] = $item->fields['id'];
                    }
                }

                if (!$abort) {
                     $update = [
                         'id'   => $params['items_id'],
                         'plan' => [
                             'begin' => $params['start'],
                             'end'   => $params['end']
                         ]
                     ];

                     if (isset($item->fields['users_id_tech'])) {
                         $update['users_id_tech'] = $item->fields['users_id_tech'];
                     }

                     // manage moving event between resource (actors)
                     if (
                         isset($params['new_actor_itemtype'])
                         && isset($params['new_actor_items_id'])
                         && !empty($params['new_actor_itemtype'])
                         && !empty($params['new_actor_items_id'])
                     ) {
                         $new_actor_itemtype = strtolower($params['new_actor_itemtype']);

                         // reminders don't have group assignement for planning
                         if (
                             !($new_actor_itemtype === 'group'
                             && $item instanceof Reminder)
                         ) {
                             switch ($new_actor_itemtype) {
                                 case "group":
                                        $update['groups_id_tech'] = $params['new_actor_items_id'];
                                     if (strtolower($params['old_actor_itemtype']) === "user") {
                                         $update['users_id_tech']  = 0;
                                     }
                                     break;

                                 case "user":
                                     if (isset($item->fields['users_id_tech'])) {
                                         $update['users_id_tech']  = $params['new_actor_items_id'];
                                         if (strtolower($params['old_actor_itemtype']) === "group") {
                                             $update['groups_id_tech']  = 0;
                                         }
                                     } else {
                                         $update['users_id'] = $params['new_actor_items_id'];
                                     }
                                     break;
                             }
                         }

                       // special case for project tasks
                       // which have a link tables for their relation with groups/users
                         if ($item instanceof ProjectTask) {
                          // get actor for finding relation with item
                             $actor = new $params['old_actor_itemtype']();
                             $actor->getFromDB((int) $params['old_actor_items_id']);

                          // get current relation
                             $team_old = new ProjectTaskTeam();
                             $team_old->getFromDBForItems($item, $actor);

                          // if new relation already exists, delete old relation
                             $actor_new = new $params['new_actor_itemtype']();
                             $actor_new->getFromDB((int) $params['new_actor_items_id']);
                             $team_new  = new ProjectTaskTeam();
                             if ($team_new->getFromDBForItems($item, $actor_new)) {
                                 $team_old->delete([
                                     'id' => $team_old->fields['id']
                                 ]);
                             } else {
                      // else update relation
                                 $team_old->update([
                                     'id'       => $team_old->fields['id'],
                                     'itemtype' => $params['new_actor_itemtype'],
                                     'items_id' => $params['new_actor_items_id'],
                                 ]);
                             }
                         }
                     }

                     if (is_subclass_of($item, "CommonITILTask")) {
                         $parentitemtype = $item->getItilObjectItemType();
                         if (!$update["_job"] = getItemForItemtype($parentitemtype)) {
                             return;
                         }

                         $fkfield = $update["_job"]->getForeignKeyField();
                         $update[$fkfield] = $item->fields[$fkfield];
                     }

                     return $item->update($update);
                }
            }
        }

        return false;
    }

    /**
     * Clean timezone information from dates fields,
     * as fullcalendar doesn't support easily timezones, let's consider it sends raw dates
     * (remove timezone suffix), we will manage timezone directy on database
     * see https://fullcalendar.io/docs/timeZone
     *
     * @since 9.5
     *
     * @param array $params parameters send by fullcalendar
     *
     * @return array cleaned $params
     */
    public static function cleanDates(array $params = []): array
    {
        $dates_fields = [
            'start', 'begin', 'end'
        ];

        foreach ($params as $key => &$value) {
            if (in_array($key, $dates_fields)) {
                $value  = date("Y-m-d H:i:s", strtotime(trim($value, 'Z')));
            }
        }

        return $params;
    }



    /**
     * Display a Planning Item
     *
     * @param $val       Array of the item to display
     * @param $who             ID of the user (0 if all)
     * @param $type            position of the item in the time block (in, through, begin or end)
     *                         (default '')
     * @param $complete        complete display (more details) (default 0)
     *
     * @return string
     **/
    public static function displayPlanningItem(array $val, $who, $type = "", $complete = 0)
    {
        $html = "";

       // bg event shouldn't have content displayed
        if (!$complete && $_SESSION['glpi_plannings']['filters']['OnlyBgEvents']['display']) {
            return "";
        }

       // Plugins case
        if (
            isset($val['itemtype'])
            && !empty($val['itemtype'])
            && $val['itemtype'] != 'NotPlanned'
            && method_exists($val['itemtype'], "displayPlanningItem")
        ) {
            /** @var class-string $val['itemtype'] */
            $html .= $val['itemtype']::displayPlanningItem($val, $who, $type, $complete);
        }

        return $html;
    }

    /**
     * Show the planning for the central page of a user
     *
     * @param $who ID of the user
     *
     * @return void
     **/
    public static function showCentral($who)
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (
            !Session::haveRight(self::$rightname, self::READMY)
            || ($who <= 0)
        ) {
            return false;
        }

        echo "<div class='table-responsive card-table'>";
        echo "<table class='table'>";
        echo "<thead>";
        echo "<tr class='noHover'><th>";
        echo "<a href='" . $CFG_GLPI["root_doc"] . "/front/planning.php'>" . __('Your planning') . "</a>";
        echo "</th></tr>";
        echo "</thead>";

        echo "<tr class='noHover'>";
        echo "<td class='planning_on_central'>";
        self::showPlanning(false);
        echo "</td></tr>";
        echo "</table>";
        echo "</div>";
    }



   //*******************************************************************************************************************************
   // *********************************** Implementation ICAL ***************************************************************
   //*******************************************************************************************************************************

    /**
     *  Generate ical file content
     *
     * @param $who             user ID
     * @param $whogroup        group ID
     * @param $limititemtype   itemtype only display this itemtype (default '')
     *
     * @return void Outputs ical contents
     **/
    public static function generateIcal($who, $whogroup, $limititemtype = '')
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (
            ($who === 0)
            && ($whogroup === 0)
        ) {
            return;
        }

        if (!empty($CFG_GLPI["version"])) {
            $unique_id = "GLPI-Planning-" . trim($CFG_GLPI["version"]);
        } else {
            $unique_id = "GLPI-Planning-UnknownVersion";
        }

       // create vcalendar
        $vcalendar = new VCalendar();

       // $xprops = array( "X-LIC-LOCATION" => $tz );
       // iCalUtilityFunctions::createTimezone( $v, $tz, $xprops );

        $interv = [];
        $begin  = time() - MONTH_TIMESTAMP * 12;
        $end    = time() + MONTH_TIMESTAMP * 12;
        $begin  = date("Y-m-d H:i:s", $begin);
        $end    = date("Y-m-d H:i:s", $end);
        $params = [
            'genical'   => true,
            'who'       => $who,
            'whogroup'  => $whogroup,
            'begin'     => $begin,
            'end'       => $end
        ];

        if (empty($limititemtype)) {
            foreach ($CFG_GLPI['planning_types'] as $itemtype) {
                $interv = array_merge($interv, $itemtype::populatePlanning($params));
            }
        } else {
            $interv = $limititemtype::populatePlanning($params);
        }

        if (count($interv) > 0) {
            foreach ($interv as $key => $val) {
                if (isset($val['itemtype'])) {
                    if (isset($val[getForeignKeyFieldForItemType($val['itemtype'])])) {
                        $uid = $val['itemtype'] . "#" . $val[getForeignKeyFieldForItemType($val['itemtype'])];
                    } else {
                        $uid = "Other#" . $key;
                    }
                } else {
                    $uid = "Other#" . $key;
                }

                $vevent['UID']     = $uid;

                $dateBegin = new DateTime($val["begin"]);
                $dateBegin->setTimeZone(new DateTimeZone('UTC'));

                $dateEnd = new DateTime($val["end"]);
                $dateEnd->setTimeZone(new DateTimeZone('UTC'));

                $vevent['DTSTART'] = $dateBegin;
                $vevent['DTEND']   = $dateEnd;

                $summary = '';
                if (isset($val["tickets_id"])) {
                    $summary = sprintf(__('Ticket #%1$s %2$s'), $val["tickets_id"], $val["name"]);
                } else if (isset($val["name"])) {
                    $summary = $val["name"];
                }
                $vevent['SUMMARY'] = $summary;

                $description = '';
                if (isset($val["content"])) {
                    $description = $val["content"];
                } else if (isset($val["text"])) {
                    $description = $val["text"];
                } else if (isset($val["name"])) {
                    $description = $val["name"];
                }
                $vevent['DESCRIPTION'] = RichText::getTextFromHtml($description);

                if (isset($val["url"])) {
                    $vevent['URL'] = $val["url"];
                }
                $vcalendar->add('VEVENT', $vevent);
            }
        }

        $output   = $vcalendar->serialize();
        $filename = date('YmdHis') . '.ics';

        @header("Content-Disposition: attachment; filename=\"$filename\"");
       //@header("Content-Length: ".Toolbox::strlen($output));
        @header("Connection: close");
        @header("content-type: text/calendar; charset=utf-8");

        echo $output;
    }

    /**
     * @since 0.85
     **/
    public function getRights($interface = 'central')
    {

        $values[self::READMY]    = __('See personnal planning');
        $values[self::READGROUP] = __('See schedule of people in my groups');
        $values[self::READALL]   = __('See all plannings');

        return $values;
    }

    /**
     * Save the last view used in fullcalendar
     *
     * @since 9.5
     *
     * @param string $view_name
     * @return void
     */
    public static function viewChanged($view_name = "ListView")
    {
        $_SESSION['glpi_plannings']['lastview'] = $view_name;
    }

    /**
     * Returns actor type from 'planning' key (key comes from user 'plannings' field).
     *
     * @param string $key
     *
     * @return string|null
     */
    public static function getActorTypeFromPlanningKey($key)
    {
        if (preg_match('/group_\d+_users/', $key)) {
            return Group_User::getType();
        }
        $itemtype = ucfirst(preg_replace('/^([a-z]+)_\d+$/', '$1', $key));
        return class_exists($itemtype) ? $itemtype : null;
    }

    /**
     * Returns actor id from 'planning' key (key comes from user 'plannings' field).
     *
     * @param string $key
     *
     * @return integer|null
     */
    public static function getActorIdFromPlanningKey($key)
    {
        $items_id = preg_replace('/^[a-z]+_(\d+)(?:_[a-z]+)?$/', '$1', $key);
        return is_numeric($items_id) ? (int)$items_id : null;
    }

    /**
     * Returns planning key for given actor (key is used in user 'plannings' field).
     *
     * @param string  $itemtype
     * @param integer $items_id
     *
     * @return string
     */
    public static function getPlanningKeyForActor($itemtype, $items_id)
    {
        if ('Group_User' === $itemtype) {
            return 'group_' . $items_id . '_users';
        }

        return strtolower($itemtype) . '_' . $items_id;
    }

    /**
     * Get CalDAV base calendar URL for given actor.
     *
     * @param CommonDBTM $item
     *
     * @return string|null
     */
    private static function getCaldavBaseCalendarUrl(\CommonDBTM $item)
    {

        $calendar_uri = null;

        switch (get_class($item)) {
            case \Group::class:
                $calendar_uri = \Glpi\CalDAV\Backend\Calendar::PREFIX_GROUPS
                 . '/' . $item->fields['id']
                 . '/' . \Glpi\CalDAV\Backend\Calendar::BASE_CALENDAR_URI;
                break;
            case \User::class:
                $calendar_uri = \Glpi\CalDAV\Backend\Calendar::PREFIX_USERS
                . '/' . $item->fields['name']
                . '/' . \Glpi\CalDAV\Backend\Calendar::BASE_CALENDAR_URI;
                break;
        }

        return $calendar_uri;
    }

    public static function getIcon()
    {
        return "ti ti-calendar-time";
    }
}
