#!/usr/bin/env php
<?php
/**
 * This script imports Open-Xchange calendars into Kronolith.
 *
 * The first argument must be the API endpoint of an Open-Xchange servers,
 * usually something like http://servername/ajax.
 *
 * If called with three arguments, the further arguments must be the user name
 * (usually "Administrator") and password of an administrator user to import
 * public calendars.
 *
 * If called with two arguments, the second argument must be a file with user
 * names and cleartext passwords separated by spaces.
 *
 * Copyright 2014-2016 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.horde.org/licenses/gpl.
 *
 * @author Jan Schneider <jan@horde.org>
 */

// Init application.
if (file_exists(__DIR__ . '/../../kronolith/lib/Application.php')) {
    $baseDir = __DIR__ . '/../';
} else {
    require_once 'PEAR/Config.php';
    $baseDir = PEAR_Config::singleton()
        ->get('horde_dir', null, 'pear.horde.org') . '/kronolith/';
}
require_once $baseDir . 'lib/Application.php';
Horde_Registry::appInit('kronolith', array('cli' => true, 'user_admin' => true));

// Read command line parameters.
if ($argc < 3 || $argc > 4) {
    $cli->message('Too many or too few parameters.', 'cli.error');
    $cli->writeln('Usage: kronolith-import-openxchange url [ file | user password ]');
    $cli->writeln($cli->indent('url is the URL of an Open-Xchange AJAX endpoint'));
    $cli->writeln($cli->indent('file is a file with space separated user names and passwords to import'));
    $cli->writeln($cli->indent('personal calendars.'));
    $cli->writeln($cli->indent('user and password are credentials of an administrator user to import public'));
    $cli->writeln($cli->indent('calendars.'));
    exit;
}
$admin = $argc == 4;

// Basic objects and variables.
$endpoint = parse_url($argv[1]);
$cli->message('Opening endpoint ' . $argv[1]);
$ox = new Horde_OpenXchange_Events(array('endpoint' => $argv[1]));
$users = array();

// Prepare handle on user/password list.
if ($admin) {
    $fp = fopen('php://temp', 'r+');
    fwrite($fp, $argv[2] . ' ' . $argv[3]);
    rewind($fp);
} else {
    if (!is_readable($argv[2]) || !filesize($argv[2])) {
        $cli->message($argv[2] . ' is not readable or empty', 'cli.error');
        exit(1);
    }
    $fp = fopen($argv[2], 'r');
}
if (!$fp) {
    exit(1);
}

// Loop through all users.
while ($row = fgetcsv($fp, 0, ' ')) {
    $user = $row[0];
    if (is_null($user)) {
        continue;
    }
    $ox->logout();
    $ox->login($user, $row[1]);

    $registry->setAuth($user, array());
    $cli->message('Importing ' . $user . '\'s calendars');

    // Reset user prefs
    $prefs = $injector->getInstance('Horde_Core_Factory_Prefs')
        ->create('kronolith', array(
            'cache' => false,
            'user' => $user
        )
    );
    $calendar_manager = $injector->createInstance('Kronolith_CalendarsManager');

    // Load calendars for current user.
    $targets = Kronolith::listInternalCalendars(true, Horde_Perms::EDIT);

    $count = 0;
    $calendars = $ox->listResources(
        $admin
            ? Horde_OpenXchange_Events::RESOURCE_PUBLIC
            : Horde_OpenXchange_Events::RESOURCE_PRIVATE
    );
    $default = $ox->getConfig('folder/calendar');

    // Loop through all calendars.
    foreach ($calendars as $folderId => $calendar) {
        // Check if we already have an calendar matching the name.
        $target = null;
        foreach ($targets as $id => $info) {
            if ($calendar['label'] == $info->get('name')) {
                $target = $id;
                break;
            }
        }
        if ($target) {
            $cli->message('Calendar "' . $calendar['label'] . '" found, updating...');
        } else {
            // Create new calendar.
            $cli->message('Calendar "' . $calendar['label'] . '" not found, creating...');
            $params = array(
                'name' => $calendar['label'],
                'color' => Kronolith::randomColor(),
                'description' => '',
                'system' => $admin,
                'tags' => array(),
            );
            $share = Kronolith::addShare($params);
            foreach ($calendar['hordePermission']['group'] as $group => $perm) {
                $share->addGroupPermission($group, $perm);
            }
            foreach ($calendar['hordePermission']['user'] as $user => $perm) {
                $share->addUserPermission($user, $perm);
            }
            $target = $share->getName();
        }

        if ($folderId == $default) {
            $prefs->setValue('default_share', $target);
        }

        // Initiate driver.
        try {
            $calendar = $calendar_manager->getEntry(
                Kronolith::ALL_CALENDARS,
                $target
            );
            $driver = Kronolith::getDriver(null, $calendar->share()->getName());
        } catch (Kronolith_Exception $e) {
            $cli->message('  ' . sprintf(_("Connection failed: %s"), $e->getMessage()), 'cli.error');
            continue;
        }

        $events = $ox->listEvents($folderId);

        // Loop through all events.
        foreach ($events as $event) {
            if ($event['recur_id'] && ($event['recur_id'] == $event['id'])) {
                // Workaround because recurrence_master parameter doesn't work
                // as advertised.
                if ($event['recur_position'] > 1) {
                    continue;
                }

                switch ($event['recur_type']) {
                case 3:
                    if ($event['day_in_month']) {
                        $event['recur_type'] = Horde_Date_Recurrence::RECUR_MONTHLY_DATE;
                    } else {
                        $event['recur_type'] = Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY;
                    }
                    break;
                case 4:
                    if ($event['day_in_month']) {
                        $event['recur_type'] = Horde_Date_Recurrence::RECUR_YEARLY_DATE;
                    } else {
                        $event['recur_type'] = Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY;
                    }
                    break;
                }
                if ($event['recur_end']) {
                    $end = new Horde_Date($event['recur_end'] / 1000, 'UTC');
                    $end->mday++;
                    $event['recur_end_date'] = $end->format('Y-m-d');
                }
                $event['recur_data'] = $event['recur_days'];
                $exceptions = array_merge(
                    (array)$event['recur_delete_exceptions'],
                    (array)$event['recur_change_exceptions']
                );
                $event['recur_exceptions'] = array();
                foreach ($exceptions as $exception) {
                    $exception = new Horde_Date($exception / 1000, 'UTC');
                    $event['recur_exceptions'][] = $exception->format('Y-m-d');
                }
            }

            $start = new Horde_Date($event['start'] / 1000, 'UTC');
            $event['start_date'] = $start->format('Y-m-d');
            $event['start_time'] = $start->format('H:i:s');
            $end = new Horde_Date($event['end'] / 1000, 'UTC');
            $event['end_date'] = $end->format('Y-m-d');
            $event['end_time'] = $end->format('H:i:s');
            $event['tags'] == $event['categories'];

            $newEvent = $driver->getEvent();
            $newEvent->fromHash($event);

            if ($event['recur_id'] && ($event['recur_id'] != $event['id'])) {
                $base = $ox->getEvent($folderId, $event['recur_id']);
                $baseStart = new Horde_Date($base['start_date'] / 1000, 'UTC');
                $baseStart->setTimezone($base['timezone']);
                $newEvent->baseid = $base['uid'];
                $newEvent->exceptionoriginaldate = new Horde_Date(
                    $event['recur_change_exceptions'][0] / 1000, 'UTC'
                );
                $newEvent->exceptionoriginaldate->setTimezone($base['timezone']);
                // This is a hack that will probably break if recurrences span
                // DST changes. Correct, but also more complex would be to
                // calculate the actual time of this recurrence.
                // recur_change_exception only contains the date.
                $newEvent->exceptionoriginaldate->hour = $baseStart->hour;
                $newEvent->exceptionoriginaldate->min = $baseStart->min;
                $newEvent->exceptionoriginaldate->sec = $baseStart->sec;
                $newEvent->uid = null;
            }
            $newEvent->timezone = $timezone;
            switch ($event['status']) {
            case 1:
                $newEvent->status = Kronolith::STATUS_CONFIRMED;
                break;
            case 2:
                $newEvent->status = Kronolith::STATUS_TENTATIVE;
                break;
            case 3:
                $newEvent->status = Kronolith::STATUS_CONFIRMED;
                break;
            case 4:
                $newEvent->status = Kronolith::STATUS_FREE;
                break;
            }
            if ($event['attendees']) {
                foreach ($event['users'] as $attendee) {
                    if (!isset($users[$attendee['id']])) {
                        $users[$attendee['id']] = $ox->getUser($attendee['id']);
                    }
                    if (count($event['attendees']) == 1 &&
                        $users[$attendee['id']]['login_info'] == $user) {
                        break;
                    }
                    $newEvent->addAttendee(
                        $users[$attendee['id']]['email1'],
                        Kronolith::PART_REQUIRED,
                        $attendee['confirmation'] + 1,
                        $users[$attendee['id']]['display_name']
                    );
                }
                foreach ($event['confirmations'] as $attendee) {
                    $newEvent->addAttendee(
                        $attendee['mail'],
                        Kronolith::PART_REQUIRED,
                        $attendee['status'] + 1,
                        $attendee['display_name']
                    );
                }
            }

            try {
                $newEvent->save();
                $count++;
            } catch (Kronolith_Exception $e) {
                $cli->message('  ' . $e->getMessage(), 'cli.error');
            }
        }
    }

    $cli->message('  Added ' . $count . ' events', 'cli.success');
    $count = 0;
}
