/* -*- Mode: C -*-
    ======================================================================
    FILE: icalspanlist.c
    CREATOR: ebusboom 23 aug 2000
  
    $Id: icalspanlist.c,v 1.15 2008-01-02 20:07:42 dothebart Exp $
    $Locker:  $
    
    (C) COPYRIGHT 2000, Eric Busboom, http://www.softwarestudio.org
    
    This program 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.fsf.org/copyleft/lesser.html
    
    Or:
    
    The Mozilla Public License Version 1.0. You may obtain a copy of
    the License at http://www.mozilla.org/MPL/


    ======================================================================*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <libical/ical.h>
#include "icalspanlist.h"

#include <stdlib.h> /* for free and malloc */
#include <string.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;

  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 ((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);
	
	printf("#%02d %d start: %s",++i,s->is_busy,ctime(&s->start));
	printf("      end  : %s",ctime(&s->end));

    }
}

icalcomponent* icalspanlist_make_free_list(icalspanlist* sl);
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(s->start,0);
	 } else {
	     period.end =  icaltime_from_timet(s->end,0);
	 }

	 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->is_busy == 0 && s->start >= rangett && 
	    ( rangett < s->end || s->end == s->start)){

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

	    return period;
	}
			
    }

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

     return period;
}

struct icalperiodtype icalspanlist_next_busy_time(icalspanlist* sl,
						struct icaltimetype t);


/** @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;
  int spanduration_secs;
  int *matrix;
  int 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(sizeof(int) * matrix_slots);
  if (matrix == NULL) {
    icalerror_set_errno(ICAL_NEWFAILED_ERROR);
    return NULL;
  }
  memset(matrix, 0, 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->is_busy == 1) {
      int offset_start = s->start/delta_t - sl_start/delta_t;
      int offset_end   = (s->end - 1) /delta_t  - sl_start/delta_t + 1;
      int i;
      
      if (offset_end >= matrix_slots)
	offset_end = matrix_slots - 1;

      i = offset_start;
      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( time(0),0);
  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->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   c        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;
}
