<?php
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Furkan Mustafa, 2015.04.06
- Updated 2015.04.09: Limit lines to 70 chars (spec is 75)
- Updated 2015.04.26: duplicate letter fixed by @PGallagher69 (Peter Gallagher)
- Updated 2015.04.26: Outtlook Invite fixed by @PGallagher69 (Peter Gallagher)
- Updated 2015.05.02: Line-limit bug fixed by @waddyvic (Victor Huang)

Alec M., 2022.02.05
- Updated 2022.02.05: General readability improvements, cleanup, and add namespace

Adapted from: https://gist.github.com/jakebellacera/635416
Also see: https://www.ietf.org/rfc/rfc5545.txt
*/

namespace amattu;

/**
 * ICS class
 */
class SimpleICS {
  use SimpleICS_Util;

  /**
   * ICS file mime type
   *
   * @var string
   */
  public const MIME_TYPE = 'text/calendar; charset=utf-8';

  /**
   * ICS event ID prefix
   *
   * NOTE:
   *  (1) This can be a empty string
   *
   * @var string
   */
  public const EVENT_PREFIX = 'SICS';

  /**
   * An array of events in the ICS file
   *
   * @var array
   */
  protected $events = [];

  /**
   * A string containing the ICS file generator string
   *
   * @var string
   */
  protected $productString = '-//amattu/SimpleICS//NONSGML v1.0//EN';

  /**
   * A string containing the ICS file template
   *
   * @var string
   */
  public static $Template = null;

  /**
   * Class Constructor
   *
   * @param string $productString
   */
  public function __construct($productString = null)
  {
    if ($productString) {
      $this->productString = $productString;
    }
  }

  /**
   * Add a new event to the ICS file
   *
   * @param array|SimpleICS_Event $eventOrClosure
   * @return SimpleICS_Event
   */
  public function addEvent($eventOrClosure) : SimpleICS_Event
  {
    // Instantiate ICS event class
    if (is_object($eventOrClosure) && ($eventOrClosure instanceof \Closure)) {
      $event = new SimpleICS_Event();
      $eventOrClosure($event);
    }

    // Push Event to Array
    $this->events[] = $event;
    return $event;
  }

  /**
   * Turn the ICS event array into a string
   *
   * @return string
   */
  public function serialize() : string
  {
    return $this->filter_linelimit($this->render(self::$Template, $this));
  }
}

/**
 * ICS event class
 */
class SimpleICS_Event {
  use SimpleICS_Util;

  /**
   * Event ID
   *
   * @var string
   */
  public $uniqueId;

  /**
   * Event Start DateTime
   *
   * @var \DateTime
   */
  public $startDate;

  /**
   * Event End DateTime
   *
   * @var \DateTime
   */
  public $endDate;

  /**
   * Event TimeStamp
   *
   * @var \DateTime
   */
  public $dateStamp;

  /**
   * Event Location
   *
   * @var string
   */
  public $location;

  /**
   * Event Description
   *
   * @var string
   */
  public $description;

  /**
   * Event URL/URI
   *
   * @var string
   */
  public $uri;

  /**
   * Event Summary (Title)
   *
   * @var string
   */
  public $summary;

  /**
   * Event ICS Template
   *
   * @var string
   */
  public static $Template;

  /**
   * Class Constructor
   */
  public function __construct()
  {
    $this->uniqueId = uniqid(SimpleICS::EVENT_PREFIX);
  }

  /**
   * Turn the ICS event into a string
   *
   * @return string
   */
  public function serialize() : string
  {
    return $this->render(self::$Template, $this);
  }
}

/**
 * SimpleICS Utility Trait
 */
trait SimpleICS_Util {
  /**
   * Shorten each input line to specified limit
   *
   * @param string $input
   * @param ?integer $lineLimit
   * @return string $output
   */
  function filter_linelimit($input, $lineLimit = 70)
  {
    // Variables
    $output = '';
    $line = '';
    $pos = 0;

    // Iterate over string
    while ($pos < strlen($input)) {
      // Find newlines
      $newLinepos = strpos($input, "\n", $pos + 1);
      if (!$newLinepos)
        $newLinepos = strlen($input);
      $line = substr($input, $pos, $newLinepos - $pos);

      if (strlen($line) <= $lineLimit) {
        $output .= $line;
      } else {
        // First line cut-off limit is $lineLimit
        $output .= substr($line, 0, $lineLimit);
        $line = substr($line, $lineLimit);

        // Subsequent line cut-off limit is $lineLimit - 1 due to the leading white space
        $output .= "\n " . substr($line, 0, $lineLimit - 1);

        while (strlen($line) > $lineLimit - 1){
          $line = substr($line, $lineLimit - 1);
          $output .= "\n " . substr($line, 0, $lineLimit - 1);
        }
      }
      $pos = $newLinepos;
    }

    return $output;
  }

  /**
   * Format the event date
   *
   * @param \DateTime|string $input
   * @return string $output formatted date
   */
  function filter_calDate($input) : string
  {
    if (!is_a($input, 'DateTime')) {
      $input = new \DateTime($input);
    } else {
      $input = clone $input;
    }

    // Format and return
    $input->setTimezone(new \DateTimeZone('UTC'));
    return $input->format('Ymd\THis\Z');
  }

  /**
   * Convert various input types to a string
   *
   * @param string $input
   * @return string $output
   */
  function filter_serialize($input)
  {
    if (is_object($input)) {
      return $input->serialize();
    }
    if (is_array($input)) {
      $output = '';
      array_walk($input, function($item) use (&$output) {
        $output .= $this->filter_serialize($item);
      });
      return trim($output, "\r\n");
    }
    return $input;
  }

  /**
   * Escape quotes in a string
   *
   * @param string $input
   * @return string $output
   */
  function filter_quote($input)
  {
    return quoted_printable_encode($input);
  }

  /**
   * Escape content from a string
   *
   * @param string $input
   * @return string $output
   */
  function filter_escape($input)
  {
    $input = preg_replace('/([\,;])/','\\\$1', $input);
    $input = str_replace("\n", "\\n", $input);
    $input = str_replace("\r", "\\r", $input);
    return $input;
  }

  function render($tpl, $scope)
  {
    while (preg_match("/\{\{([^\|\}]+)((?:\|([^\|\}]+))+)?\}\}/", $tpl, $m)) {
      $replace = $m[0];
      $varname = $m[1];
      $filters = isset($m[2]) ? explode('|', trim($m[2], '|')) : [];

      $value = $this->fetch_var($scope, $varname);
      $self = &$this;
      array_walk($filters, function(&$item) use (&$value, $self) {
        $item = trim($item, "\t\r\n ");
        if (!is_callable([ $self, 'filter_' . $item ]))
          throw new \Exception('No such filter: ' . $item);

        $value = call_user_func_array([ $self, 'filter_' . $item ], [ $value ]);
      });

      $tpl = str_replace($m[0], $value, $tpl);
    }
    return $tpl;
  }

  function fetch_var($scope, $var)
  {
    if (strpos($var, '.')!==false) {
      $split = explode('.', $var);
      $var = array_shift($split);
      $rest = implode('.', $split);
      $val = $this->fetch_var($scope, $var);
      return $this->fetch_var($val, $rest);
    }

    if (is_object($scope)) {
      $getterMethod = 'get' . ucfirst($var);
      if (method_exists($scope, $getterMethod)) {
        return $scope->{$getterMethod}();
      }
      return $scope->{$var};
    }

    if (is_array($scope))
      return $scope[$var];

    throw new \Exception('A strange scope');
  }
}

/**
 * ICS Content Template
 *
 * @var string
 */
SimpleICS::$Template = <<<EOT
BEGIN:VCALENDAR
VERSION:2.0
PRODID:{{productString}}
METHOD:PUBLISH
CALSCALE:GREGORIAN
{{events|serialize}}
END:VCALENDAR

EOT;

/**
 * ICS Event Content Template
 *
 * @var string
 */
SimpleICS_Event::$Template = <<<EOT
BEGIN:VEVENT
UID:{{uniqueId}}
DTSTART:{{startDate|calDate}}
DTSTAMP:{{dateStamp|calDate}}
DTEND:{{endDate|calDate}}
LOCATION:{{location|escape}}
DESCRIPTION:{{description|escape}}
URL;VALUE=URI:{{uri|escape}}
SUMMARY:{{summary|escape}}
END:VEVENT

EOT;