<?php
/**
 * Copyright 2011-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author  Jan Schneider <jan@horde.org>
 * @package Timezone
 */

/**
 * Class representing a set of "Rule" timezone database entries of the
 * same name.
 *
 * @author  Jan Schneider <jan@horde.org>
 * @package Timezone
 */
class Horde_Timezone_Zone
{
    /**
     * The timezone ID.
     *
     * @var string
     */
    protected $_name;

    /**
     * A Horde_Timezone object.
     *
     * @va Horde_Timezone
     */
    protected $_tz;

    /**
     * Zone lines of this zone object.
     *
     * @var array
     */
    protected $_info = array();

    /**
     * Constructor.
     *
     * @param string $name        A timezone ID.
     * @param Horde_Timezone $tz  A Horde_Timezone object. Used to retrieve
     *                            rules for this timezone.
     */
    public function __construct($name, Horde_Timezone $tz)
    {
        $this->setTzid($name);
        $this->_tz = $tz;
    }

    /**
     * Sets the timezone ID.
     *
     * There are aliases for timezone IDs, it might be necessary to
     * use one of those.
     *
     * @param string $name        A timezone ID.
     */
    public function setTzid($name)
    {
        $this->_name = $name;
    }

    /**
     * Adds a Zone line to this zone object.
     *
     * @param array $info  A parsed Zone line or continuation line.
     */
    public function add($info)
    {
        $this->_info[] = $info;
    }

    /**
     * Exports this zone to a VTIMEZONE component.
     *
     * @param Horde_Date $from  The earliest date that we need timezone
     *                          definitions for.
     * @param Horde_Date $to    The latest date.
     *
     * @return Horde_Icalendar_Vtimezone  A VTIMEZONE component representing
     *                                    this timezone.
     * @throws Horde_Timezone_Exception
     */
    public function toVtimezone($from = null, $to = null)
    {
        if (!count($this->_info)) {
            throw new Horde_Timezone_Exception('No rules found for timezone ' . $this->_name);
        }

        $tz = new Horde_Icalendar_Vtimezone();
        $tz->setAttribute('TZID', $this->_name);
        if (count($this->_info[0]) <= 3) {
            // Zone has no start or end date, but DAYLIGHT and STANDARD
            // components are required to have a DTSTART attribute [RFC
            // 5545 3.8.2.4].
            return $tz;
        }

        $startDate = $this->_getDate(0);
        $startOffset = $this->_getOffset(0);
        for ($i = 1, $c = count($this->_info); $i < $c; $i++) {
            if ($to && $startDate->after($to)) {
                continue;
            }
            $name = $this->_info[$i][2];
            $endDate = count($this->_info[$i]) > 3 ? $this->_getDate($i) : null;
            if ($from && $endDate && $endDate->before($from)) {
                continue;
            }
            if ($this->_info[$i][1] == '-') {
                // Standard time.
                $component = new Horde_Icalendar_Standard();
            } elseif (preg_match('/\d+(:(\d+))?/', $this->_info[$i][1])) {
                // Indiviual rule not matching any ruleset.
                $component = new Horde_Icalendar_Daylight();
            } else {
                // Represented by a ruleset.
                $startOffset = $this->_getOffset($i);
                $this->_tz->getRule($this->_info[$i][1])
                    ->addRules(
                        $tz,
                        $this->_name,
                        $name,
                        $startOffset,
                        $from && $from->after($startDate) ? $from : $startDate,
                        $to && $to->before($endDate) ? $to : $endDate
                    );
                $startDate = $endDate;
                // Continue, because addRules() already adds the
                // component to $tz.
                continue;
            }
            $component->setAttribute('DTSTART', $startDate);
            $component->setAttribute('TZOFFSETFROM', $startOffset);
            $startOffset = $this->_getOffset($i);
            $component->setAttribute('TZOFFSETTO', $startOffset);
            $component->setAttribute('TZNAME', $name);
            $tz->addComponent($component);
        }

        return $tz;
    }

    /**
     * Calculates a date from the date columns of a Zone line.
     *
     * @param integer $line  A line number.
     *
     * @return Horde_Date  The date of this line.
     */
    protected function _getDate($line)
    {
        $date = array_slice($this->_info[$line], 3);
        $year = $date[0];
        $month = isset($date[1]) ? Horde_Timezone::getMonth($date[1]) : 1;
        $day = isset($date[2]) ? $date[2] : 1;
        $time = isset($date[3]) && $date[3] != '-' ? $date[3] : 0;
        preg_match('/(\d+)(?::(\d+))?(?::(\d+))?(w|s|u)?/', $time, $match);
        if (!isset($match[2])) {
            $match[2] = 0;
        }
        if (!isset($match[3])) {
            $match[3] = 0;
        }
        switch (substr($time, -1)) {
        case 's':
            // Standard time. Not sure what to do about this.
            break;
        case 'u':
            // UTC, add offset.
            $offset = $this->_getOffset($line);
            $factor = $offset['ahead'] ? 1 : -1;
            $match[1] += $factor * $offset['hour'];
            $match[2] += $factor * $offset['minute'];
        case 'w':
        default:
            // Wall time, nothing to do.
            break;
        }
        return new Horde_Date(array('year'  => $year,
                                    'month' => $month,
                                    'mday'  => $day,
                                    'hour'  => $match[1],
                                    'min'   => $match[2],
                                    'sec'   => $match[3]));
    }

    /**
     * Calculates an offset from the offset column of a Zone line.
     *
     * @param integer $line  A line number.
     *
     * @return array  A hash describing the offset.
     */
    protected function _getOffset($line)
    {
        $offset = $this->_info[$line][0];
        preg_match('/(-)?(\d+):(\d+)/', $offset, $match);
        return array('ahead'  => $match[1] != '-',
                     'hour'   => $match[2],
                     'minute' => $match[3]);
    }
}
