/*======================================================================
 FILE: icalspanlist.c
 CREATOR: ebusboom 23 aug 2000

 (C) COPYRIGHT 2000, Eric Busboom <eric@softwarestudio.org>
     http://www.softwarestudio.org

 This library is free software; you can redistribute it and/or modify
 it under the terms of either:

    The LGPL as published by the Free Software Foundation, version
    2.1, available at: http://www.gnu.org/licenses/lgpl-2.1.html

 Or:

    The Mozilla Public License Version 2.0. You may obtain a copy of
    the License at http://www.mozilla.org/MPL/
 ======================================================================*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "icalspanlist.h"
#include "icaltimezone.h"

#include <stdlib.h>

struct icalspanlist_impl
{
    pvl_list spans;             /**< list of icaltime_span data **/
    struct icaltimetype start;  /**< start time of span **/
    struct icaltimetype end;    /**< end time of span **/
};

/** @brief Internal comparison function for two spans
 *
 *  @param  a   a spanlist.
 *  @param  b   another spanlist.
 *
 *  @return     -1, 0, 1 depending on the comparison of the start times.
 *
 * Used to insert spans into the tree in sorted order.
 */

static int compare_span(void *a, void *b)
{
    struct icaltime_span *span_a = (struct icaltime_span *)a;
    struct icaltime_span *span_b = (struct icaltime_span *)b;

    if (span_a->start == span_b->start) {
        return 0;
    } else if (span_a->start < span_b->start) {
        return -1;
    } else {    /*if(span_a->start > span->b.start) */
        return 1;
    }
}

/** @brief callback function for collecting spanlists of a
 *         series of events.
 *
 *  @param   comp  A valid icalcomponent.
 *  @param   span  The span to insert into data.
 *  @param   data  The actual spanlist to insert into
 *
 *  This callback is used by icalcomponent_foreach_recurrence()
 *  to build up a spanlist.
 */

static void icalspanlist_new_callback(icalcomponent *comp, struct icaltime_span *span, void *data)
{
    icaltime_span *s;
    icalspanlist *sl = (icalspanlist *) data;

    _unused(comp);

    if (span->is_busy == 0)
        return;

    if ((s = (icaltime_span *) malloc(sizeof(icaltime_span))) == 0) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        return;
    }

  /** copy span data into allocated memory.. **/
    *s = *span;
    pvl_insert_ordered(sl->spans, compare_span, (void *)s);
}

/** @brief Make a free list from a set of VEVENT components.
 *
 *  @param set    A valid icalset containing VEVENTS
 *  @param start  The free list starts at this date/time
 *  @param end    The free list ends at this date/time
 *
 *  @return        A spanlist corresponding to the VEVENTS
 *
 * Given a set of components, a start time and an end time
 * return a spanlist that contains the free/busy times.
 */

icalspanlist *icalspanlist_new(icalset *set, struct icaltimetype start, struct icaltimetype end)
{
    struct icaltime_span range;
    pvl_elem itr;
    icalcomponent *c, *inner;
    icalcomponent_kind kind, inner_kind;
    icalspanlist *sl;
    struct icaltime_span *freetime;

    if ((sl = (struct icalspanlist_impl *)malloc(sizeof(struct icalspanlist_impl))) == 0) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        return 0;
    }

    sl->spans = pvl_newlist();
    sl->start = start;
    sl->end = end;

    range.start = icaltime_as_timet(start);
    range.end = icaltime_as_timet(end);

    /* Get a list of spans of busy time from the events in the set
       and order the spans based on the start time */

    for (c = icalset_get_first_component(set);
         c != 0;
         c = icalset_get_next_component(set)) {

        kind = icalcomponent_isa(c);
        inner = icalcomponent_get_inner(c);

        if (!inner)
            continue;

        inner_kind = icalcomponent_isa(inner);

        if (kind != ICAL_VEVENT_COMPONENT && inner_kind != ICAL_VEVENT_COMPONENT)
            continue;

        icalerror_clear_errno();

        icalcomponent_foreach_recurrence(c, start, end, icalspanlist_new_callback, (void *)sl);
    }

    /* Now Fill in the free time spans. loop through the spans. if the
       start of the range is not within the span, create a free entry
       that runs from the start of the range to the start of the
       span. */

    for (itr = pvl_head(sl->spans); itr != 0; itr = pvl_next(itr)) {
        struct icaltime_span *s = (struct icaltime_span *)pvl_data(itr);

        if (!s)
            continue;

        if ((freetime = (struct icaltime_span *)malloc(sizeof(struct icaltime_span))) == 0) {
            icalerror_set_errno(ICAL_NEWFAILED_ERROR);
            icalspanlist_free(sl);
            return 0;
        }

        if (range.start < s->start) {
            freetime->start = range.start;
            freetime->end = s->start;

            freetime->is_busy = 0;

            pvl_insert_ordered(sl->spans, compare_span, (void *)freetime);
        } else {
            free(freetime);
        }

        range.start = s->end;
    }

    /* If the end of the range is null, then assume that everything
       after the last item in the calendar is open and add a span
       that indicates this */

    if (icaltime_is_null_time(end)) {
        struct icaltime_span *last_span;

        last_span = (struct icaltime_span *)pvl_data(pvl_tail(sl->spans));

        if (last_span != 0) {

            if ((freetime = (struct icaltime_span *)malloc(sizeof(struct icaltime_span))) == 0) {
                icalerror_set_errno(ICAL_NEWFAILED_ERROR);
                icalspanlist_free(sl);
                return 0;
            }

            freetime->is_busy = 0;
            freetime->start = last_span->end;
            freetime->end = freetime->start;
            pvl_insert_ordered(sl->spans, compare_span, (void *)freetime);
        }
    }

    return sl;
}

/** @brief Destructor.
 *  @param s A valid icalspanlist
 *
 *  Free memory associated with the spanlist
 */

void icalspanlist_free(icalspanlist *s)
{
    struct icaltime_span *span;

    if (s == NULL)
        return;

    while ((span = pvl_pop(s->spans)) != 0) {
        free(span);
    }

    pvl_free(s->spans);

    s->spans = 0;

    free(s);
}

/** @brief (Debug) print out spanlist to stdout.
 *  @param sl A valid icalspanlist.
 */

void icalspanlist_dump(icalspanlist *sl)
{
    int i = 0;
    pvl_elem itr;

    for (itr = pvl_head(sl->spans); itr != 0; itr = pvl_next(itr)) {
        struct icaltime_span *s = (struct icaltime_span *)pvl_data(itr);
        if (s) {
            printf("#%02d %d start: %s", ++i, s->is_busy, ctime(&s->start));
            printf("      end  : %s", ctime(&s->end));
        }
    }
}

icalcomponent *icalspanlist_make_busy_list(icalspanlist *sl);

/** @brief Find next free time span in a spanlist.
 *
 *  @param  sl     The spanlist to search.
 *  @param  t      The time to start looking.
 *
 *  Given a spanlist and a time, find the next period of time
 *  that is free
 */

struct icalperiodtype icalspanlist_next_free_time(icalspanlist *sl, struct icaltimetype t)
{
    pvl_elem itr;
    struct icalperiodtype period;
    struct icaltime_span *s;

    time_t rangett = icaltime_as_timet(t);

    period.start = icaltime_null_time();
    period.end = icaltime_null_time();

    itr = pvl_head(sl->spans);
    s = (struct icaltime_span *)pvl_data(itr);

    if (s == 0) {
        /* No elements in span */
        return period;
    }

    /* Is the reference time before the first span? If so, assume
       that the reference time is free */
    if (rangett < s->start) {
        /* End of period is start of first span if span is busy, end
           of the span if it is free */
        period.start = t;

        if (s->is_busy == 1) {
            period.end = icaltime_from_timet_with_zone(s->start, 0, NULL);
        } else {
            period.end = icaltime_from_timet_with_zone(s->end, 0, NULL);
        }

        return period;
    }

    /* Otherwise, find the first free span that contains the
       reference time. */
    for (itr = pvl_head(sl->spans); itr != 0; itr = pvl_next(itr)) {
        s = (struct icaltime_span *)pvl_data(itr);

        if (!s)
            continue;

        if (s->is_busy == 0 && s->start >= rangett && (rangett < s->end || s->end == s->start)) {

            if (rangett < s->start) {
                period.start = icaltime_from_timet_with_zone(s->start, 0, NULL);
            } else {
                period.start = icaltime_from_timet_with_zone(rangett, 0, NULL);
            }

            period.end = icaltime_from_timet_with_zone(s->end, 0, NULL);

            return period;
        }
    }

    period.start = icaltime_null_time();
    period.end = icaltime_null_time();

    return period;
}

/** @brief Returns an hour-by-hour array of free/busy times over a
 *         given period.
 *
 *  @param sl        A valid icalspanlist
 *  @param delta_t   The time slice to divide by, in seconds.  Default 3600.
 *
 *  @return A pointer to an array of integers containing the number of
 *       busy events in each delta_t time period.  The final entry
 *       contains the value -1.
 *
 *  This calculation is somewhat tricky.  This is due to the fact that
 *  the time range contains the start time, but does not contain the
 *  end time.  To perform a proper calculation we subtract one second
 *  off the end times to get a true containing time.
 *
 *  Also note that if you supplying a spanlist that does not start or
 *  end on a time boundary divisible by delta_t you may get results
 *  that are not quite what you expect.
 */

int *icalspanlist_as_freebusy_matrix(icalspanlist *sl, int delta_t)
{
    pvl_elem itr;
    time_t spanduration_secs;
    int *matrix;
    time_t matrix_slots;
    time_t sl_start, sl_end;

    icalerror_check_arg_rz((sl != 0), "spanlist");

    if (!delta_t)
        delta_t = 3600;

  /** calculate the start and end time as time_t **/
    sl_start = icaltime_as_timet_with_zone(sl->start, icaltimezone_get_utc_timezone());
    sl_end = icaltime_as_timet_with_zone(sl->end, icaltimezone_get_utc_timezone());

  /** insure that the time period falls on a time boundary divisable
      by delta_t */

    sl_start /= delta_t;
    sl_start *= delta_t;

    sl_end /= delta_t;
    sl_end *= delta_t;

  /** find the duration of this spanlist **/
    spanduration_secs = sl_end - sl_start;

  /** malloc our matrix, add one extra slot for a final -1 **/
    matrix_slots = spanduration_secs / delta_t + 1;

    matrix = (int *)malloc((size_t)(sizeof(int) * matrix_slots));
    if (matrix == NULL) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        return NULL;
    }
    memset(matrix, 0, (size_t)(sizeof(int) * matrix_slots));
    matrix[matrix_slots - 1] = -1;

    /* loop through each span and mark the slots in the array */

    for (itr = pvl_head(sl->spans); itr != 0; itr = pvl_next(itr)) {
        struct icaltime_span *s = (struct icaltime_span *)pvl_data(itr);

        if (s && s->is_busy == 1) {
            time_t offset_start = s->start / delta_t - sl_start / delta_t;
            time_t offset_end = (s->end - 1) / delta_t - sl_start / delta_t + 1;
            time_t i;

            if (offset_end >= matrix_slots)
                offset_end = matrix_slots - 1;

            for (i = offset_start; i < offset_end; i++) {
                matrix[i]++;
            }
        }
    }
    return matrix;
}

/** @brief Return a VFREEBUSY component for the corresponding spanlist
 *
 *   @param sl         A valid icalspanlist, from icalspanlist_new()
 *   @param organizer  The organizer specified as "MAILTO:user@domain"
 *   @param attendee   The attendee specified as "MAILTO:user@domain"
 *
 *   @return            A valid icalcomponent or NULL.
 *
 * This function returns a VFREEBUSY component for the given spanlist.
 * The start time is mapped to DTSTART, the end time to DTEND.
 * Each busy span is represented as a separate FREEBUSY entry.
 * An attendee parameter is required, and organizer parameter is
 * optional.
 */

icalcomponent *icalspanlist_as_vfreebusy(icalspanlist *sl,
                                         const char *organizer, const char *attendee)
{
    icalcomponent *comp;
    icalproperty *p;
    struct icaltimetype atime = icaltime_from_timet_with_zone(time(0), 0, NULL);
    pvl_elem itr;
    icaltimezone *utc_zone;
    icalparameter *param;

    if (!attendee) {
        icalerror_set_errno(ICAL_USAGE_ERROR);
        return 0;
    }

    utc_zone = icaltimezone_get_utc_timezone();

    comp = icalcomponent_new_vfreebusy();

    icalcomponent_add_property(comp, icalproperty_new_dtstart(sl->start));
    icalcomponent_add_property(comp, icalproperty_new_dtend(sl->end));
    icalcomponent_add_property(comp, icalproperty_new_dtstamp(atime));

    if (organizer) {
        icalcomponent_add_property(comp, icalproperty_new_organizer(organizer));
    }
    icalcomponent_add_property(comp, icalproperty_new_attendee(attendee));

    /* now add the freebusy sections.. */

    for (itr = pvl_head(sl->spans); itr != 0; itr = pvl_next(itr)) {
        struct icalperiodtype period;
        struct icaltime_span *s = (struct icaltime_span *)pvl_data(itr);

        if (s && s->is_busy == 1) {

            period.start = icaltime_from_timet_with_zone(s->start, 0, utc_zone);
            period.end = icaltime_from_timet_with_zone(s->end, 0, utc_zone);
            period.duration = icaldurationtype_null_duration();

            p = icalproperty_new_freebusy(period);
            param = icalparameter_new_fbtype(ICAL_FBTYPE_BUSY);
            icalproperty_add_parameter(p, param);

            icalcomponent_add_property(comp, p);
        }
    }

    return comp;
}

/** @brief Return a spanlist corresponding to the VFREEBUSY portion of
 *         an icalcomponent.
 *
 *   @param   comp     A valid icalcomponent.
 *
 *   @return           A valid icalspanlist or NULL if no VFREEBUSY section.
 *
 */

icalspanlist *icalspanlist_from_vfreebusy(icalcomponent *comp)
{
    icalcomponent *inner;
    icalproperty *prop;
    icalspanlist *sl;

    icalerror_check_arg_rz((comp != NULL), "comp");

    inner = icalcomponent_get_inner(comp);
    if (!inner)
        return NULL;

    if ((sl = (icalspanlist *) malloc(sizeof(icalspanlist))) == 0) {
        icalerror_set_errno(ICAL_NEWFAILED_ERROR);
        return 0;
    }
    sl->spans = pvl_newlist();

    /* cycle through each FREEBUSY property, adding to the spanlist */
    for (prop = icalcomponent_get_first_property(inner, ICAL_FREEBUSY_PROPERTY);
         prop != NULL;
         prop = icalcomponent_get_next_property(inner, ICAL_FREEBUSY_PROPERTY)) {
        icaltime_span *s = (icaltime_span *) malloc(sizeof(icaltime_span));
        icalparameter *param;
        struct icalperiodtype period;
        icalparameter_fbtype fbtype;

        if (s == 0) {
            icalerror_set_errno(ICAL_NEWFAILED_ERROR);
            icalspanlist_free(sl);
            return 0;
        }

        param = icalproperty_get_first_parameter(prop, ICAL_FBTYPE_PARAMETER);
        fbtype = (param) ? icalparameter_get_fbtype(param) : ICAL_FBTYPE_BUSY;

        switch (fbtype) {
        case ICAL_FBTYPE_FREE:
        case ICAL_FBTYPE_NONE:
        case ICAL_FBTYPE_X:
            s->is_busy = 1;
            break;
        default:
            s->is_busy = 0;
        }

        period = icalproperty_get_freebusy(prop);
        s->start = icaltime_as_timet_with_zone(period.start, icaltimezone_get_utc_timezone());
        s->end = icaltime_as_timet_with_zone(period.end, icaltimezone_get_utc_timezone());
        ;
        pvl_insert_ordered(sl->spans, compare_span, (void *)s);
    }
  /** @todo calculate start/end limits.. fill in holes? **/
    return sl;
}
