<?php
/**
 * The Kronolith_Driver_sql:: class implements the Kronolith_Driver
 * API for a SQL backend.
 *
 * $Horde: kronolith/lib/Driver/sql.php,v 1.88 2003/06/19 18:47:42 chuck Exp $
 *
 * @author  Luc Saillard <luc.saillard@fr.alcove.com>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.88 $
 * @since   Kronolith 0.3
 * @package kronolith
 */
class Kronolith_Driver_sql extends Kronolith_Driver {

    /**
     * The object handle for the current database connection.
     * @var object DB $_db
     */
    var $_db;

    /**
     * Boolean indicating whether or not we're currently connected to
     * the SQL server.
     * @var boolean $_connected
     */
    var $_connected = false;

    function open($calendar)
    {
        $this->_calendar = $calendar;
        $this->_connect();
    }

    function listAlarms($date)
    {
        $allevents = $this->listEvents($date, $date, true);
        $events = array();

        foreach ($allevents as $eventid) {
            $event = &$this->getEventObject($eventid);

            if ($event->getRecurType() == KRONOLITH_RECUR_NONE) {
                $diff = Kronolith::dateDiff($date,
                                            Kronolith::timestampToObject($event->startTimestamp));
                if ($diff->sec < $event->getAlarm() * 60) {
                    $events[] = $eventid;
                }
            } else {
                $next = $this->nextRecurrence($eventid, $date);
                if ($next) {
                    $diff = Kronolith::dateDiff($date, $next);
                    if ($diff->sec < $event->getAlarm() * 60) {
                        $events[] = $eventid;
                    }
                }
            }
        }

        return is_array($events) ? $events : array();
    }

    function listEvents($startDate = null, $endDate = null, $hasAlarm = false)
    {
        $events = array();

        if (!isset($endDate)) {
            $endDate = Kronolith::dateObject(array('mday' => 31, 'month' => 12, 'year' => 9999));
        } else {
            list($endDate->mday, $endDate->month, $endDate->year) = explode('/', Date_Calc::nextDay($endDate->mday, $endDate->month, $endDate->year, '%d/%m/%Y'));
        }

        $etime = sprintf('%04d-%02d-%02d 00:00:00', $endDate->year, $endDate->month, $endDate->mday);
        if (isset($startDate)) {
            if ($startDate === 0) {
                $startDate = Kronolith::dateObject(array('mday' => 1, 'month' => 1, 'year' => 0000));
            }
            if ($startDate->month == 0) { $startDate->month = 1; }
            if ($startDate->mday == 0) { $startDate->mday = 1; }
            $stime = sprintf('%04d-%02d-%02d 00:00:00', $startDate->year, $startDate->month, $startDate->mday);
        }

        $q = 'SELECT DISTINCT e.event_id, e.event_recurtype FROM ' . $this->_params['table'] . ' e' .
             ' WHERE e.calendar_id = ' . $this->_db->quote($this->_calendar) . ' AND ((';

        if ($hasAlarm) {
            $q .= 'e.event_alarm > 0)) AND ((';
        }

        if (isset($stime)) {
            $q .= 'e.event_end > ' . $this->_db->quote($stime) . ' AND ';
        }
        $q .= 'e.event_start < ' . $this->_db->quote($etime) . ') OR (';
        if (isset($stime)) {
            $q .= 'e.event_recurenddate >= ' . $this->_db->quote($stime) . ' AND ';
        }
        $q .= 'e.event_start <= ' . $this->_db->quote($etime) .
              ' AND e.event_recurtype != ' . KRONOLITH_RECUR_NONE . '))';

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('SQL event list by %s: table = %s; query = "%s"',
                                  Auth::getAuth(), $this->_params['table'], $q),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

        /* Run the query. */
        $qr = $this->_db->query($q);

        if (!is_a($qr, 'PEAR_Error')) {
            $row = $qr->fetchRow(DB_FETCHMODE_ASSOC);
            while ($row && !is_a($row, 'PEAR_Error')) {
                if ($row['event_recurtype'] == KRONOLITH_RECUR_NONE) {
                    $events[] = $row['event_id'];
                } else {
                    $next = $this->nextRecurrence($row['event_id'], $startDate);
                    if ($next && Kronolith::compareDates($next, $endDate) < 0) {
                        $events[] = $row['event_id'];
                    }
                }

                $row = $qr->fetchRow(DB_FETCHMODE_ASSOC);
            }
        }

        return $events;
    }

    function &getEventObject($eventID = null)
    {
        if (!is_null($eventID)) {
            $event = &$this->_db->getRow('SELECT event_id, event_description, event_location,' .
                                         ' event_keywords, event_title, event_category,' .
                                         ' event_recurtype, event_recurenddate, event_recurinterval,' .
                                         ' event_recurdays, event_start, event_end, event_alarm,' .
                                         ' event_modified, event_exceptions, event_creator_id' .
                                         ' FROM ' . $this->_params['table'] .
                                         ' WHERE event_id = ' . (int)$eventID .
                                         ' AND calendar_id = ' . $this->_db->quote($this->_calendar),
                                         DB_FETCHMODE_ASSOC);

            if (is_a($event, 'PEAR_Error')) {
                return $event;
            }

            if ($event) {
                return new Kronolith_Event_sql($this, $event);
            } else {
                return false;
            }
        } else {
            return new Kronolith_Event_sql($this);
        }
    }

    function saveEvent($event)
    {
        if (!is_null($event->getID())) {
            $query = 'UPDATE ' . $this->_params['table'] . ' SET ';

            foreach ($event->getProperties() as $key => $val) {
                $query .= " $key = " . $this->_db->quote($val) . ',';
            }
            $query = substr($query, 0, -1);
            $query .= ' WHERE event_id = ' . $this->_db->quote((int)$event->getID());

            /* Log the query at a DEBUG log level. */
            Horde::logMessage(sprintf('SQL event update by %s: table = %s; query = "%s"',
                                      Auth::getAuth(), $this->_params['table'], $query),
                              __FILE__, __LINE__, PEAR_LOG_DEBUG);

            $res = $this->_db->query($query);
            if (is_a($res, 'PEAR_Error')) {
                return $res;
            }

            return $event->getID();
        } else {
            $id = $this->_db->nextId($this->_params['table']);
            if (is_a($id, 'PEAR_Error')) {
                return $id;
            }

            $query = 'INSERT INTO ' . $this->_params['table'] . ' ';
            $cols_name = '(event_id,';
            $cols_values = 'values (' . $this->_db->quote($id) . ',';

            foreach ($event->getProperties() as $key => $val) {
                $cols_name .= " $key,";
                $cols_values .= $this->_db->quote($val) . ',';
            }

            $cols_name .= ' calendar_id)';
            $cols_values .= $this->_db->quote($this->_calendar) . ')';

            /* Log the query at a DEBUG log level. */
            Horde::logMessage(sprintf('SQL event store by %s: table = %s; query = "%s"',
                                Auth::getAuth(), $this->_params['table'], $query . $cols_name . $cols_values),
                                __FILE__, __LINE__, PEAR_LOG_DEBUG);

            $res = $this->_db->query($query . $cols_name . $cols_values);
            if (is_a($res, 'PEAR_Error')) {
                return $res;
            }

            return $id;
        }
    }

    function nextRecurrence($eventID, $afterDate, $weekstart = KRONOLITH_SUNDAY)
    {
        $daysInMonth = Kronolith::daysInMonth($afterDate['month'], $afterDate['year']);
        while ($afterDate['mday'] > $daysInMonth) {
            $afterDate['mday'] -= $daysInMonth;
            $afterDate['month']++;
            if ($afterDate['month'] > 12) {
                $afterDate['year']++;
                $afterDate['month'] -= 12;
            }
        }

        $event = &$this->getEventObject($eventID);
        if (is_a($event, 'PEAR_Error')) {
            return $event;
        }

        $afterDate = Kronolith::dateObject($afterDate);

        if (Kronolith::compareDates($event->start, $afterDate) > 0) {
            return $event->start;
        }

        $event->recurEnd->hour = 23;
        $event->recurEnd->min  = 59;
        $event->recurEnd->sec  = 59;

        switch ($event->getRecurType()) {

        case KRONOLITH_RECUR_DAILY:
            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = (ceil($diff->mday / $event->recurInterval)) * $event->recurInterval;
            $next = $event->start;
            list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        case KRONOLITH_RECUR_WEEKLY:
            list($start_week->mday, $start_week->month, $start_week->year) = explode('/', Date_Calc::beginOfWeek($event->start->mday, $event->start->month, $event->start->year, '%e/%m/%Y'));
            $start_week->hour = $event->start->hour;
            $start_week->min = $event->start->min;
            $start_week->sec = $event->start->sec;
            list($after_week->mday, $after_week->month, $after_week->year) = explode('/', Date_Calc::beginOfWeek($afterDate->mday, $afterDate->month, $afterDate->year, '%e/%m/%Y'));
            $after_week_end = $after_week;
            $after_week_end->mday += 7;
            $after_week_end = Kronolith::correctDate($after_week_end);
            $diff = Kronolith::dateDiff($start_week, $after_week);
            $recur = $diff->mday + $diff->mday % ($event->recurInterval * 7);
            $next = $start_week;
            list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
            while (Kronolith::compareDates($next, $afterDate) < 0 && Kronolith::compareDates($next, $after_week_end) < 0) {
                $next->mday++;
                $next = Kronolith::correctDate($next);
            }
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0) {
                if (Kronolith::compareDates($next, $after_week_end) >= 0) {
                    return $this->nextRecurrence($eventID, $after_week_end);
                }
                while (!$event->recurOnDay((int)pow(2, (int)Date_Calc::dayOfWeek($next->mday, $next->month, $next->year))) && Kronolith::compareDates($next, $after_week_end) < 0) {
                    $next->mday++;
                    $next = Kronolith::correctDate($next);
                }
                if (Kronolith::compareDates($next, $event->recurEnd) <= 0) {
                    if (Kronolith::compareDates($next, $after_week_end) >= 0) {
                        return $this->nextRecurrence($eventID, $after_week_end);
                    } else {
                        return Kronolith::dateObject($next);
                    }
                }
            }
            break;

        case KRONOLITH_RECUR_DAY_OF_MONTH:
            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = $diff->month + $diff->month % $event->recurInterval;
            $next = $event->start;
            $next->month += $recur;
            $next = Kronolith::correctDate($next);
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        case KRONOLITH_RECUR_WEEK_OF_MONTH:
            $week = ceil($event->start->mday / 7);
            $wday = date('w', Kronolith::objectToTimestamp($event->start));

            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = $diff->month + $diff->month % $event->recurInterval;
            $next = $event->start;
            $next->month += $recur;
            $next = Kronolith::correctDate($next);
            $next->mday = $week*7;
            $next_wday = date('w', Kronolith::objectToTimestamp($next));
            if ($next_wday > $wday) {
                $next->mday -= ($next_wday - $wday);
            } elseif ($next_wday < $wday) {
                $next->mday -= ($next_wday + (7 - $wday));
            }

            // Correct the data and return it if w/in the requested range
            $next = Kronolith::correctDate($next);
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        case KRONOLITH_RECUR_YEARLY:
            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = $diff->year + $diff->year % $event->recurInterval;
            $next = $event->start;
            $next->year += $recur;
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        }

        return false;
    }

    /**
     * Delete a calendar and all its events.
     *
     * @param string $calendar The name of the calendar to delete.
     *
     * @return mixed  True or a PEAR_Error on failure.
     */
    function delete($calendar)
    {
        $this->_connect();

        $query = sprintf('DELETE FROM %s WHERE calendar_id = %s',
                    $this->_params['table'],
                    $this->_db->quote($calendar));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('SQL Calender Delete by %s: table = %s; query = "%s"',
                                  Auth::getAuth(), $this->_params['table'], $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

        return $this->_db->query($query);
    }

    /**
     * Delete an event.
     *
     * @param string $eventID The id of the event to delete.
     *
     * @return mixed  True or a PEAR_Error on failure.
     */
    function deleteEvent($eventID)
    {
        $eventID = (int)$eventID;
        $query = sprintf('DELETE FROM %s WHERE event_id = %s AND calendar_id = %s',
                         $this->_params['table'],
                         $this->_db->quote($eventID),
                         $this->_db->quote($this->_calendar));

        /* Log the query at a DEBUG log level. */
        Horde::logMessage(sprintf('SQL Event Delete by %s: table = %s; query = "%s"',
                                  Auth::getAuth(), $this->_params['table'], $query),
                          __FILE__, __LINE__, PEAR_LOG_DEBUG);

        return $this->_db->query($query);
    }

    /**
     * Attempts to open a persistent connection to the SQL server.
     *
     * @return boolean True.
     */
    function _connect()
    {
        if (!$this->_connected) {
            require_once 'DB.php';

            if (!is_array($this->_params)) {
                Horde::fatal(PEAR::raiseError(_("No configuration information specified for SQL Calendar.")), __FILE__, __LINE__);
            }
            if (!isset($this->_params['phptype'])) {
                Horde::fatal(PEAR::raiseError(_("Required 'phptype' not specified in calendar configuration.")), __FILE__, __LINE__);
            }
            if (!isset($this->_params['hostspec'])) {
                Horde::fatal(PEAR::raiseError(_("Required 'hostspec' not specified in calendar configuration.")), __FILE__, __LINE__);
            }
            if (!isset($this->_params['username'])) {
                Horde::fatal(PEAR::raiseError(_("Required 'username' not specified in calendar configuration.")), __FILE__, __LINE__);
            }
            if (!isset($this->_params['password'])) {
                Horde::fatal(PEAR::raiseError(_("Required 'password' not specified in calendar configuration.")), __FILE__, __LINE__);
            }

            /* Connect to the SQL server using the supplied parameters. */
            $this->_db = &DB::connect($this->_params,
                                      array('persistent' => !empty($this->_params['persistent'])));
            if (is_a($this->_db, 'PEAR_Error')) {
                Horde::fatal($this->_db, __FILE__, __LINE__);
            }

            /* Enable the "portability" option. */
            $this->_db->setOption('optimize', 'portability');

            $this->_connected = true;

            /* Handle any database specific initialization code to
             * run. */
            switch ($this->_db->dbsyntax) {
            case 'oci8':
                $query = "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'";

                /* Log the query at a DEBUG log level. */
                Horde::logMessage(sprintf('SQL session setup by %s: table = %s; query = "%s"',
                                          Auth::getAuth(), $this->_params['table'], $query),
                                  __FILE__, __LINE__, PEAR_LOG_DEBUG);

                $this->_db->query($query);
                break;
            }
        }

        return true;
    }

    function close()
    {
        return true;
    }

    /**
     * Disconnect from the SQL server and clean up the connection.
     *
     * @return boolean true on success, false on failure.
     */
    function _disconnect()
    {
        if ($this->_connected) {
            $this->_connected = false;
            return $this->_db->disconnect();
        }

        return true;
    }

}

class Kronolith_Event_sql extends Kronolith_Event {

    var $_properties = array();

    function fromDriver($SQLEvent)
    {
        $driver = &$this->getDriver();

        $this->start = new stdClass();
        $this->end = new stdClass();
        list($this->start->year, $this->start->month, $this->start->mday, $this->start->hour, $this->start->min, $this->start->sec) = sscanf($SQLEvent['event_start'], '%04d-%02d-%02d %02d:%02d:%02d');
        list($this->end->year, $this->end->month, $this->end->mday, $this->end->hour, $this->end->min, $this->end->sec) = sscanf($SQLEvent['event_end'], '%04d-%02d-%02d %02d:%02d:%02d');

        $this->startTimestamp = mktime($this->start->hour, $this->start->min, $this->start->sec, $this->start->month, $this->start->mday, $this->start->year);
        $this->endTimestamp = mktime($this->end->hour, $this->end->min, $this->end->sec, $this->end->month, $this->end->mday, $this->end->year);

        $this->durMin = ($this->endTimestamp - $this->startTimestamp) / 60;

        if (isset($SQLEvent['event_recurenddate'])) {
            $this->recurEnd = new stdClass();
            list($this->recurEnd->year, $this->recurEnd->month, $this->recurEnd->mday, $this->recurEnd->hour, $this->recurEnd->min, $this->recurEnd->sec) = sscanf($SQLEvent['event_recurenddate'], '%04d-%02d-%02d %02d:%02d:%02d');
            $this->recurEndTimestamp = @mktime($this->recurEnd->hour, $this->recurEnd->min, $this->recurEnd->sec, $this->recurEnd->month, $this->recurEnd->mday, $this->recurEnd->year);
        }

        $this->title = String::convertCharset($SQLEvent['event_title'], $driver->_params['charset']);
        $this->eventID = $SQLEvent['event_id'];
        $this->creatorID = $SQLEvent['event_creator_id'];
        $this->recurType = (int)$SQLEvent['event_recurtype'];
        $this->recurInterval = (int)$SQLEvent['event_recurinterval'];

        if (isset($SQLEvent['event_category'])) {
            $this->category = $SQLEvent['event_category'];
        }
        if (isset($SQLEvent['event_location'])) {
            $this->location = String::convertCharset($SQLEvent['event_location'], $driver->_params['charset']);
        }
        if (isset($SQLEvent['event_keywords'])) {
            $this->keywords = explode(',', String::convertCharset($SQLEvent['event_keywords'], $driver->_params['charset']));
        }
        if (isset($SQLEvent['event_exceptions'])) {
            $this->exceptions = explode(',', $SQLEvent['event_exceptions']);
        }
        if (isset($SQLEvent['event_description'])) {
            $this->description = String::convertCharset($SQLEvent['event_description'], $driver->_params['charset']);
        }
        if (isset($SQLEvent['event_alarm'])) {
            $this->alarm = (int)$SQLEvent['event_alarm'];
        }
        if (isset($SQLEvent['event_recurdays'])) {
            $this->recurData = (int)$SQLEvent['event_recurdays'];
        }

        $this->initialized = true;
    }

    function toDriver()
    {
        $driver = &$this->getDriver();

        // Basic fields.
        $this->_properties['event_creator_id'] = String::convertCharset($this->getCreatorID(), NLS::getCharset(), $driver->_params['charset']);
        $this->_properties['event_title'] = String::convertCharset($this->getTitle(), NLS::getCharset(), $driver->_params['charset']);
        $this->_properties['event_description'] = String::convertCharset($this->getDescription(), NLS::getCharset(), $driver->_params['charset']);
        $this->_properties['event_category'] = $this->getCategory();
        $this->_properties['event_location'] = String::convertCharset($this->getLocation(), NLS::getCharset(), $driver->_params['charset']);
        $this->_properties['event_keywords'] = String::convertCharset(implode(',', $this->getKeywords()), NLS::getCharset(), $driver->_params['charset']);
        $this->_properties['event_exceptions'] = implode(',', $this->getExceptions());
        $this->_properties['event_modified'] = time();
        $this->_properties['event_modified'] = time();

        // Event start.
        $this->_properties['event_start'] = date('Y-m-d H:i:s', $this->getStartTimestamp());

        // Event end.
        $this->_properties['event_end'] = date('Y-m-d H:i:s', $this->getEndTimestamp());

        // Alarm.
        $this->_properties['event_alarm'] = $this->getAlarm();

        // Recurrence.
        $recur_end = explode(':', @date('Y:n:j', $this->getRecurEndTimestamp()));
        if (empty($recur_end[0]) || $recur_end[0] <= 1970) {
            $recur_end[0] = 9999;
            $recur_end[1] = 12;
            $recur_end[2] = 31;
        }

        $recur = $this->getRecurType();
        $this->_properties['event_recurtype'] = $recur;
        if ($recur != KRONOLITH_RECUR_NONE) {
            $this->_properties['event_recurinterval'] = $this->getRecurInterval();
            $this->_properties['event_recurenddate'] = sprintf('%04d%02d%02d', $recur_end[0],
                                                               $recur_end[1], $recur_end[2]);

            switch ($recur) {
            case KRONOLITH_RECUR_WEEKLY:
                $this->_properties['event_recurdays'] = $this->getRecurOnDays();
                break;
            }
        }
    }

    function getProperties()
    {
        return $this->_properties;
    }

}
