<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2022 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/>.
 * ---------------------------------------------------------------------
 */

namespace Glpi\CalDAV\Traits;

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

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

/**
 * Trait containing methods to convert properties from/to a VObject component.
 *
 * @since 9.5.0
 */
trait VobjectConverterTrait {

   /**
    * Get VCalendar object for given item.
    *
    * @param \CommonDBTM $item
    * @param string      $component_type  Base component type (i.e. VEVENT, VTODO, ...).
    *
    * @return VCalendar
    */
   protected function getVCalendarForItem(\CommonDBTM $item, $component_type): VCalendar {
      global $CFG_GLPI;

      if (!array_key_exists($component_type, VCalendar::$componentMap)) {
         throw new \InvalidArgumentException(sprintf('Invalid component type "%s"', $component_type));
      }

      $vobject = new \VObject();
      $vobject_crit = [
         'items_id' => $item->fields['id'],
         'itemtype' => $item->getType(),
      ];

      // Restore previously saved VCalendar if available
      $vcalendar = null;
      $vcomp     = null;
      if ($vobject->getFromDBByCrit($vobject_crit) && !empty($vobject->fields['data'])) {
         $vcalendar = Reader::read($vobject->fields['data']);
         $vcomp = $vcalendar->getBaseComponent();
         if (VCalendar::$componentMap[$component_type] !== get_class($vcomp)) {
            // Remove existing base component if it has changed.
            // For instance component can change when depending on state of a PlanningExternalEvent.
            $vcalendar->remove($vcomp);
            $vcomp = null;
         }
      }
      if (!($vcalendar instanceof VCalendar)) {
         $vcalendar = new VCalendar();
      }
      if (!($vcomp instanceof Component)) {
         $vcomp = $vcalendar->add($component_type);
      }

      $fields = \Html::entity_decode_deep($item->fields);
      $utc_tz = new \DateTimeZone('UTC');

      if (array_key_exists('uuid', $fields)) {
         $vcomp->UID = $fields['uuid'];
      }

      if (array_key_exists('date_creation', $fields)) {
         $vcomp->CREATED = (new \DateTime($fields['date_creation']))->setTimeZone($utc_tz);
      } else if (array_key_exists('date', $fields)) {
         $vcomp->CREATED = (new \DateTime($fields['date']))->setTimeZone($utc_tz);
      }

      if (array_key_exists('date_mod', $fields)) {
         $vcomp->DTSTAMP           = (new \DateTime($fields['date_mod']))->setTimeZone($utc_tz);
         $vcomp->{'LAST-MODIFIED'} = (new \DateTime($fields['date_mod']))->setTimeZone($utc_tz);
      }

      if (array_key_exists('name', $fields)) {
         $vcomp->SUMMARY = $fields['name'];
      }

      if (array_key_exists('content', $fields)) {
         $vcomp->DESCRIPTION = $fields['content'];
      } else if (array_key_exists('text', $fields)) {
         $vcomp->DESCRIPTION = $fields['text'];
      }

      $vcomp->URL = $CFG_GLPI['url_base'] . $this->getFormURLWithID($fields['id'], false);

      if (array_key_exists('begin', $fields) && !empty($fields['begin'])) {
         $vcomp->DTSTART = (new \DateTime($fields['begin']))->setTimeZone($utc_tz);
      }

      if (array_key_exists('end', $fields) && !empty($fields['end'])) {
         $end_date = (new \DateTime($fields['end']))->setTimeZone($utc_tz);
         if ('VTODO' === $component_type) {
            $vcomp->DUE = $end_date;
         } else {
            $vcomp->DTEND = $end_date;
         }
      }

      if (array_key_exists('rrule', $fields) && !empty($fields['rrule'])) {
         $rrule_specs = json_decode($fields['rrule'], true);
         try {
            if (array_key_exists('byweekday', $rrule_specs)) {
               $rrule_specs['byday'] = $rrule_specs['byweekday'];
               unset($rrule_specs['byweekday']);
            }
            if (array_key_exists('until', $rrule_specs) && empty($rrule_specs['until'])) {
               unset($rrule_specs['until']);
            }
            if (array_key_exists('exceptions', $rrule_specs)) {
               foreach ($rrule_specs['exceptions'] as $exdate) {
                  $vcomp->add('EXDATE', (new \DateTime($exdate))->setTimeZone($utc_tz));
               }
               unset($rrule_specs['exceptions']);
            }
            $rrule = new RRule($rrule_specs);
            $vcomp->RRULE = $rrule->rfcString();
         } catch (\InvalidArgumentException $e) {
            \Toolbox::logError(
               sprintf('Invalid RRULE "%s" from event "%s"', $fields['rrule'], $fields['id'])
            );
         }
      }

      if ('VTODO' === $component_type && array_key_exists('state', $fields)) {
         if (\Planning::TODO == $fields['state']) {
            $vcomp->STATUS = 'NEEDS-ACTION';
         } else if (\Planning::DONE == $fields['state']) {
            $vcomp->STATUS = 'COMPLETED';
         }
      }

      return $vcalendar;
   }

   /**
    * Return the most relevant caldav component according to configuration.
    *
    * @param boolean $is_planned
    * @param boolean $is_task
    *
    * @return string|null
    */
   protected function getTargetCaldavComponent(bool $is_planned, bool $is_task) {
      global $CFG_GLPI;

      // Use VTODO for tasks if available.
      if ($is_task && in_array('VTODO', $CFG_GLPI['caldav_supported_components'])) {
         return 'VTODO';
      }

      // Use VEVENT for planned items if available (it includes tasks if VTODO is not available).
      if ($is_planned && in_array('VEVENT', $CFG_GLPI['caldav_supported_components'])) {
         return 'VEVENT';
      }

      // Use VJOURNAL for unplanned items if available (it includes tasks if VTODO is not available).
      if (!$is_planned && in_array('VJOURNAL', $CFG_GLPI['caldav_supported_components'])) {
         return 'VJOURNAL';
      }

      // No component fits item properties
      return null;
   }

   /**
    * Get common item input for given component.
    *
    * @param Component $vcomponent
    * @param bool      $is_new_item
    *
    * @return array
    */
   protected function getCommonInputFromVcomponent(Component $vcomponent, bool $is_new_item = true) {
      if (!($vcomponent instanceof VEvent)
          && !($vcomponent instanceof VTodo)
          && !($vcomponent instanceof VJournal)) {
         throw new \UnexpectedValueException(
            'Component object must be a VEVENT, a VJOURNAL, or a VTODO'
         );
      }

      $input = [];

      if ($vcomponent->CREATED instanceof DateTime) {
         /* @var \DateTime|\DateTimeImmutable|null $created_datetime */
         $user_tz = new \DateTimeZone(date_default_timezone_get());
         $created_datetime = $vcomponent->CREATED->getDateTime();
         $created_datetime = $created_datetime->setTimeZone($user_tz);
         $input['date_creation'] = $created_datetime->format('Y-m-d H:i:s');
      }

      if ($vcomponent->SUMMARY instanceof FlatText) {
         $input['name'] = $vcomponent->SUMMARY->getValue();
      }

      $input['content'] = $this->getContentRichTextInputFromVComponent($vcomponent);

      $plan = $this->getPlanInputFromVComponent($vcomponent);
      if (null !== $plan) {
         $input['plan'] = $plan;
      }

      $input['rrule'] = $this->getRRuleInputFromVComponent($vcomponent);

      $state = $this->getStateInputFromVComponent($vcomponent);
      if ($state !== null) {
         $input['state'] = $state;
      } else if ($is_new_item) {
         $input['state'] = GLPI_CALDAV_IMPORT_STATE;
      }

      return $input;
   }

   /**
    * Get content input from component converted into HTML format.
    *
    * @param Component $vcomponent
    *
    * @return string|null
    */
   private function getContentRichTextInputFromVComponent(Component $vcomponent) {
      if (!($vcomponent->DESCRIPTION instanceof FlatText)) {
         return null;
      }

      $content = $vcomponent->DESCRIPTION->getValue();
      $content = nl2br($content);
      $content = str_replace(["\n", "\r"], ['', ''], $content);
      return $content;
   }

   /**
    * Get state input from component (see Planning constants).
    *
    * @param Component $vcomponent
    *
    * @return integer|null
    */
   private function getStateInputFromVComponent(Component $vcomponent) {
      if (!($vcomponent->STATUS instanceof FlatText)) {
         return null;
      }

      return 'COMPLETED' === $vcomponent->STATUS->getValue() ? \Planning::DONE : \Planning::TODO;
   }

   /**
    * Return begin/end date from component as an array object containing:
    *  - 'begin': begin date in 'Y-m-d H:i:s' format;
    *  - 'end':   end date in 'Y-m-d H:i:s' format.
    * If object does not contains plan informations, return null.
    *
    * @param Component $vcomponent
    *
    * @return array|null
    */
   private function getPlanInputFromVComponent(Component $vcomponent) {
      if (!($vcomponent->DTSTART instanceof DateTime)) {
         return null;
      }

      /* @var \DateTime|\DateTimeImmutable|null $begin_datetime */
      /* @var \DateTime|\DateTimeImmutable|null $end_datetime */
      $user_tz        = new \DateTimeZone(date_default_timezone_get());

      $begin_datetime = $vcomponent->DTSTART->getDateTime();
      $begin_datetime = $begin_datetime->setTimeZone($user_tz);

      $end_datetime   = null;
      if ($vcomponent instanceof VTodo) {
         if ($vcomponent->DUE instanceof DateTime) {
            $end_datetime = $vcomponent->DUE->getDateTime();
            $end_datetime = $end_datetime->setTimeZone($user_tz);
         }
      } else {
         if ($vcomponent->DTEND instanceof DateTime) {
            $end_datetime = $vcomponent->DTEND->getDateTime();
            $end_datetime = $end_datetime->setTimeZone($user_tz);
         }
      }
      if (!($end_datetime instanceof \DateTimeInterface)) {
         // Event/Task objects does not accept empty end date, so set it to "+1 hour" by default.
         $end_datetime = clone $begin_datetime;
         $end_datetime = $end_datetime->add(new \DateInterval('PT1H'));
      }

      return [
         'begin' => $begin_datetime->format('Y-m-d H:i:s'),
         'end'   => $end_datetime->format('Y-m-d H:i:s'),
      ];
   }

   /**
    * Get rrule input from component in format expected by events methods.
    *
    * @param Component $vcomponent
    *
    * @return array|null
    */
   private function getRRuleInputFromVComponent(Component $vcomponent) {
      if (!($vcomponent->RRULE instanceof Recur)) {
         return null;
      }

      // Get first array element which actually correspond to rrule specs
      $rrule = current($vcomponent->RRULE->getJsonValue());

      if (array_key_exists('byday', $rrule) && !is_array($rrule['byday'])) {
         // When only one day is set, sabre/vobject return a string instead of an array
         $rrule['byday'] = [$rrule['byday']];
      }

      if (array_key_exists('until', $rrule)) {
         $user_tz        = new \DateTimeZone(date_default_timezone_get());
         $until_datetime = new \DateTime($rrule['until']);
         $until_datetime->setTimezone($user_tz);
         $rrule['until'] = $until_datetime->format('Y-m-d H:i:s');
      }

      $exceptions = $vcomponent->select('EXDATE');
      if (count($exceptions) > 0) {
         $rrule['exceptions'] = [];
         foreach ($exceptions as $exdate) {
            $rrule['exceptions'][] = $exdate->getDateTime()->format('Y-m-d');
         }
         $rrule['exceptions'] = implode (', ', $rrule['exceptions']);
      }

      return $rrule;
   }
}
