<?php

/**
 * @version     $Id$
 * @package     ARTIO Booking
 * @subpackage  models
 * @copyright   Copyright (C) 2010 ARTIO s.r.o.. All rights reserved.
 * @author      ARTIO s.r.o., http://www.artio.net
 * @license     GNU/GPL http://www.gnu.org/copyleft/gpl.html
 * @link        http://www.artio.net Official website
 */

defined('_JEXEC') or die('Restricted access');

abstract class BookingModelPeriod {
    /**
     * @var BookingConfig
     */
    protected $config;
    /**
     * @var TableReservationItems
     */
    protected $item;
    /**
     * @var TableSubject
     */
    protected $subject;
    /**
     * @var TableReservationType
     */
    protected $rtype;
    /**
     * @var TablePrice
     */
    protected $price;
    /**
     * @var JDate
     */
    protected $beginDay;
    /**
     * @var JDate
     */
    protected $endDay;
    /**
     * @var JDate
     */
    protected $day;
    /**
     * @var JDate
     */
    protected $timeUp;
    /**
     * @var JDate
     */
    protected $timeDown;
    /**
     * @var DateInterval
     */
    protected $timeDiff;
    /**
     * @var int
     */
    protected $beginDayUnix;
    /**
     * @var int
     */
    protected $endDayUnix;
    /**
     * @var int
     */
    protected $quantity;

    protected $ordinal = array(1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth');
    protected $dayName = array(1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday');

    /**
     * Get period model instance according to period type.
     * @param TableReservationItems $item
     * @param TableSubject $subject
     * @param int $quantity
     * @return BookingModelPeriodDaily|BookingModelPeriodMonthly|BookingModelPeriodYearly
     * @throws Exception
     */
    public static function getInstance($item, $subject, $quantity) {
        if ($item->period_type == PERIOD_TYPE_WEEKLY || $item->period_type == PERIOD_TYPE_DAILY) {
            require_once __DIR__ . '/period/daily.php';
            return new BookingModelPeriodDaily($item, $subject, $quantity);
        } elseif ($item->period_type == PERIOD_TYPE_MONTHLY) {
            require_once __DIR__ . '/period/montly.php';
            return new BookingModelPeriodMonthly($item, $subject, $quantity);
        } elseif ($item->period_type == PERIOD_TYPE_YEARLY) {
            require_once __DIR__ . '/period/yearly.php';
            return new BookingModelPeriodYearly($item, $subject, $quantity);
        }
        throw new Exception('Undefined period type: ' . $item->period_type);
    }

    public function __construct($item, $subject, $quantity) {
        $this->config = AFactory::getConfig();
        $this->db = JFactory::getDbo();

        $this->item = $item;
        $this->subject = $subject;
        $this->quantity = $quantity;

        $this->rtype = JTable::getInstance('ReservationType', 'Table');
        $this->price = JTable::getInstance('Price', 'Table');

        $this->rtype->load($this->item->period_rtype_id);
        $this->price->load($this->item->period_price_id);

        TablePrice::prepare($this->price, $this->subject);
    }

    /**
     * Calculate whole period.
     * @throws Exception
     */
    public function calculate() {
        $this->item->period_total = 0;

        $this->beginDay = JFactory::getDate($this->item->period_date_up);
        $this->day = clone $this->beginDay;
        $this->endDay = JFactory::getDate($this->item->period_date_down);

        $this->item->period_time_up = $this->item->period_time_up ?: '00:00:00';
        $this->item->period_time_down = $this->item->period_time_down ?: '23:59:59';

        $this->timeUp = JFactory::getDate($this->item->period_time_up);
        $this->timeDown = JFactory::getDate($this->item->period_time_down);

        $this->timeDiff = $this->timeUp->diff($this->timeDown);

        if ($this->item->period_end == PERIOD_END_TYPE_NO) { // if period end undefined then compute for next 365 days
            $this->endDay = JFactory::getDate('+ 365 days');
        }

        $this->beginDayUnix = $this->beginDay->toUnix();
        $this->endDayUnix = $this->endDay->toUnix();

        if ($this->item->period_end == PERIOD_END_TYPE_AFTER) {
            $this->endDayUnix = JFactory::getDate($this->price->date_down)->toUnix();
        }

        $this->_calculate();

        if (count($this->item->period)) {
            $this->checkCovering();
            $this->checkClosing();
        }
    }

    protected function checkCovering() {
        foreach ($this->item->period as $time) {
            $up = $this->db->q(date('Y-m-d', $time) . ' ' . $this->item->period_time_up);
            $down = $this->db->q(date('Y-m-d', $time) . ' ' . $this->item->period_time_down);

            $where[] = "(p.to > $up AND p.from < $down) OR (i.to > $up AND i.from < $down)";
        }

        // check if wanted period is already covered with another reservation
        $items = $this->db->setQuery('
                  SELECT COALESCE(p.from, i.from) AS `from`, COALESCE(p.to, i.to) AS `to`, capacity, capacity AS capacity2
                  FROM #__booking_reservation AS r
                  LEFT JOIN #__booking_reservation_items AS i ON r.id = i.reservation_id
                  LEFT JOIN #__booking_reservation_period AS p ON i.id = p.reservation_item_id                                           
                  WHERE i.subject = ' . intval($this->subject->id) . ' AND r.state = ' . RESERVATION_ACTIVE . '
                  AND (' . implode(' OR ', $where) . ')')->loadObjectList();

        $this->item->conflict = array();
        foreach ($items as $i => $item) {
            if ($item->capacity + $this->quantity > $this->subject->total_capacity) {
                $this->item->conflict[] = $item->from;
                continue;
            }
            foreach ($items as $j => $item2) {
                if ($i != $j && $item->from < $item2->to && $item->to > $item2->from) {
                    $item->capacity2 += $item2->capacity; // sum quantity of covered time
                    if ($item->capacity2 + $this->quantity > $this->subject->total_capacity) {
                        $this->item->conflict[] = $item->from;
                        break;
                    }
                }
            }
        }
        if ($this->item->conflict) {
            throw new Exception(JText::_('PERIOD_BROKEN'));
        }
    }

    /**
     * Check if period is broken by some closing day.
     * @throws Exception
     */
    protected function checkClosing() {
        $model = JModelLegacy::getInstance('Closingdays', 'BookingModel');
        /* @var $model BookingModelClosingdays */
        $closingDays = $model->getSubjectClosingDays($this->subject->id);
        foreach ($closingDays as $closingDay) {
            foreach ($this->item->period as $time) {
                $time = JFactory::getDate($time);
                if ($closingDay->isClosed($time->format('Y-m-d'), $this->item->period_time_up, $this->item->period_time_down, BookingHelper::dayCodeToString($time->format('N')))) {
                    $begin = JFactory::getDate($closingDay->date_up)->setTime(1, 1, 1);
                    $end = JFactory::getDate($closingDay->date_down)->setTime(1, 1, 1);
                    while ($begin->toUnix() <= $end->toUnix()) {
                        $this->item->conflict[] = $begin->toUnix();
                        $begin->modify('next day');
                    }
                    break 2;
                }
            }
        }
        if (!empty($this->item->conflict)) {
            throw new Exception(JText::_('PERIOD_CLOSED'));
        }
    }

    /**
     * Set period items into calendar.
     * @param BookingInterval $interval
     * @param int[] $boxIds
     */
    public function setCalendar($interval, &$boxIds) {
        $interval->rtype = RESERVATION_TYPE_PERIOD;

        $day = new BookingDay();
        $box = new BookingTimeBox();
        $service = new BookingService();

        $boxIds = $box->services = array();

        $hours = $this->rtype->time_unit > 0 ? (($this->timeDiff->h * 60 + $this->timeDiff->i) / $this->rtype->time_unit) : 1;

        $service->bind($this->price, $this->rtype);
        $service->rtype = RESERVATION_TYPE_PERIOD;
        $service->priceIndex = $service->alreadyReserved = $service->fromDate = $service->toDate = 0;

        for ($i = 0; $i < $this->item->period_total * $hours; $i++) {
            $service->id = $i;
            $boxIds[] = $service->id;
            $box->services[] = $service;
        }

        $day->boxes = array($box);
        $this->rtype->prices = array($this->price);

        $interval->calendar = new stdClass();
        $interval->calendar->calendar = array($day);
        $interval->calendar->prices = array($this->rtype->id => $this->rtype);
    }

    /**
     * Get current week number word.
     * @return string
     */
    protected function getWeek() {
        if (isset($this->ordinal[$this->item->period_week])) {
            return $this->ordinal[$this->item->period_week];
        }
    }

    /**
     * Get current day name.
     * @return string
     */
    protected function getDay() {
        if (isset($this->dayName[$this->item->period_day])) {
            return $this->dayName[$this->item->period_day];
        }
    }

    /**
     * Current period is at the end.
     * @return bool
     */
    protected function isEnd() {
        return ($this->day->toUnix() > $this->endDayUnix) || ($this->item->period_end == PERIOD_END_TYPE_AFTER && $this->item->period_total == $this->item->period_occurrences);
    }

    /**
     * Add next period item.
     */
    protected function add() {
        $this->item->period_total++;
        $this->item->period[] = $this->day->toUnix();
    }
}