<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2021 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI 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 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI 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 GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

use RRule\RRule;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Property\FlatText;
use Sabre\VObject\Reader;
use Sabre\VObject\ParseException;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTodo;
use Sabre\VObject\Property\ICalendar\Recur;

/**
 * Planning Class
**/
class Planning extends CommonGLPI {

   static $rightname = 'planning';

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

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

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

   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
   **/
   static function getTypeName($nb = 0) {
      return __('Planning');
   }


   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;
   }


   static function getAdditionalMenuLinks() {
      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;
   }


   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()
            ]
         ];
      }
   }


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


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

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


   function defineTabs($options = []) {

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

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

      return $ong;
   }


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

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

         return $tabs;
      }
      return '';
   }


   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
   **/
   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');
      }
   }


   /**
    * 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
   **/
   static function dropdownState($name, $value = '', $display = true, $options = []) {

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

      return Dropdown::showFromArray($name, $values, array_merge(['value'   => $value,
                                                                  'display' => $display], $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]]
   **/
   static function checkAlreadyPlanned($users_id, $begin, $end, $except = []) {
      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
   **/
   static function checkAvailability($params = []) {
      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['users_id'] = 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='submit' 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')) {
                  $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
   **/
   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'" : "") . ">";

      // 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"),
         ];
      } 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'></div>";
      echo "</div>";

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

      return;
   }

   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
    */
   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
    */
   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() {
      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
    */
   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
    */
   static function showPlanningFilter() {
      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 "<img class='pointer' src='".$CFG_GLPI['root_doc']."/pics/add_dark.png'>";
            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>";
      echo "</div>";
   }


   /**
    * 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
    */
   static function showSingleLinePlanningFilter($filter_key, $filter_data, $options = []) {
      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;
      if ($filter_data['type'] == 'user') {
         $uID = $actor[1];
         $user = new User();
         $user->getFromDB($actor[1]);
         $title = $user->getName();
      } else if ($filter_data['type'] == 'group_users') {
         $group = new Group();
         $group->getFromDB($actor[1]);
         $title = $group->getName();
      } else if ($filter_data['type'] == 'group') {
         $gID = $actor[1];
         $group = new Group();
         $group->getFromDB($actor[1]);
         $title = $group->getName();
      } 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']."'>";
      Html::showCheckbox(['name'          => 'filters[]',
                               'value'         => $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'>$title</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']);
      }

      if ($filter_data['type'] != 'event_filter') {
         echo "<span class='filter_option'>";
         echo "<img class='pointer' src='".$CFG_GLPI['root_doc']."/pics/down.png' />";
         echo "<ul>";
         if ($params['show_delete']) {
            echo "<li class='delete_planning' value='$filter_key'>".__("Delete")."</li>";
         }
         if ($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><a target='_blank' href='".$CFG_GLPI["root_doc"]."$cal_url'>".
                 _sx("button", "Export")." - ".__("Ical")."</a></li>";

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

            echo "<li><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/'
               . self::getCaldavBaseCalendarUrl($filter_data['type'] == 'user' ? $user : $group);
            $copy_js = 'copyTextToClipboard("' . $caldav_url . '");'
               . ' alert("' . __s('CalDAV URL has been copied to clipboard') . '");'
               . ' return false;';
            echo "<li><a target='_blank' href='#'
                 onclick='$copy_js'>".
                 __s("Copy CalDAV URL to clipboard")."</a></li>";
         }
         echo "</ul>";
         echo "</span>";
      }

      // 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'] == '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
    */
   static function showAddPlanningForm() {
      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
    */
   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)
    */
   static function sendAddUserForm($params = []) {
      $_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
    */
   static function showAddGroupUsersForm() {
      echo Group::getTypeName(1)." : <br>";

      $condition = ['is_task' => 1];
      // 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)
    */
   static function sendAddGroupUsersForm($params = []) {
      $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();
   }


   static function editEventForm($params = []) {
      if (!$params['itemtype'] instanceof CommonDBTM) {
         echo "<div class='center'>";
         echo "<a href='".$params['url']."'>".__("View this item in his context")."</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 = getItemForItemtype($params['itemtype']);
         $item->showForm(intval($params['id']), $options);
         $callback = "$('.ui-dialog-content').dialog('close');
                      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
    */
   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)
    */
   static function sendAddGroupForm($params = []) {
      $_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
    */
   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
    */
   static function sendAddExternalForm($params = []) {
      $_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']++;
   }


   static function showAddEventForm($params = []) {
      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
    */
   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 = "$('.ui-dialog-content').dialog('close');
                      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)
    */
   static function showAddEventClassicForm($params = []) {
      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='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"])) {
         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
    */
   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;
         }

         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
    */
   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
    */
   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
    */
   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
    */
   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();
   }


   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
    */
   static function constructEventsArray($options = []) {
      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 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") {
               $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
    */
   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')
            ) {
               $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 {
      $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
         }
         $calendar_data = Toolbox::getURLContent($planning_params['url']);
         if (empty($calendar_data)) {
            continue;
         }
         try {
            $vcalendar = Reader::read($calendar_data);
         } catch (ParseException $exception) {
            Toolbox::logError(sprintf('Unable to parse calendar data from URL "%s"', $planning_params['url']));
            continue;
         }
         if (!$vcalendar instanceof VCalendar) {
            Toolbox::logError(sprintf('No VCalendar object found at URL "%s"', $planning_params['url']));
            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,
            ];
         }
      }

      return $raw_events;
   }


   /**
    * Change dates of a selected event.
    * Called from a drag&drop in planning
    *
    * @since 9.1
    *
    * @param array $options: must contains this keys :
    *  - items_id : integer to identify items
    *  - itemtype : string to identify items
    *  - begin : 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
    */
   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 exists and is not in bin

            $abort = false;

            // we should not edit events from closed parent
            if (!empty($item->fields['tickets_id'])) {
               // todo: to same checks for changes, problems, projects and maybe reminders and others depending on incoming itemtypes
               $ticket = new Ticket();

               if (!$ticket->getFromDB($item->fields['tickets_id'])
               || $ticket->fields['is_deleted']
               || $ticket->fields['status'] == CommonITILObject::CLOSED
               ) {
                  $abort = true;
               }
            }

            // 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)) {
                  $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);
            }
         }
      }
   }

   /**
    * 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
    */
   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
   **/
   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")) {
         $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
   **/
   static function showCentral($who) {
      global $CFG_GLPI;

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

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

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



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

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

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

      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;

            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;

            if (isset($val["content"])) {
               $description = $val["content"];
               // be sure to replace nl by \r\n
               $description = preg_replace("/<br( [^>]*)?".">/i", "\r\n", $description);
               $description = Html::clean($description);
            } else if (isset($val["text"])) {
               $description = $val["text"];
               // be sure to replace nl by \r\n
               $description = preg_replace("/<br( [^>]*)?".">/i", "\r\n", $description);
               $description = Html::clean($description);
            } else if (isset($val["name"])) {
               $description = $val["name"];
               // be sure to replace nl by \r\n
               $description = preg_replace("/<br( [^>]*)?".">/i", "\r\n", $description);
               $description = Html::clean($description);
            }
            $vevent['DESCRIPTION'] = $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
   **/
   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
    */
   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;
   }

   static function getIcon() {
      return "far fa-calendar-alt";
   }
}
