
sub check_data()
{
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    if (!(-e "$options{calendars_file}"))
      {$fatal_error=1;$error_info .= "Calendars file $options{calendars_file} not found!\n";}
    if (!(-e $options{new_calendars_file}))
      {$fatal_error=1;$error_info .= "New calendars file $options{new_calendars_file} not found!\n";}
    if (!(-e $options{events_file}))
      {$fatal_error=1;$error_info .= "Events file $options{events_file} not found!\n";}
    
    if ($fatal_error == 0)
    {  
	# Remember which files are writable.
	$writable{calendars_file} = (-w $options{calendars_file});
	$writable{new_calendars_file} = (-w $options{new_calendars_file});
	$writable{events_file} = (-w $options{events_file});
	$writable{email_reminders_datafile} = (-w $options{email_reminders_datafile});
  
	# If the events file is not writable then we shouldn't
	# show the Add/Edit events tab on the main page.
	delete($tab_text[1]) unless $writable{events_file};
    } 

  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    $writable{calendars_file} = 1;
	  $writable{new_calendars_file} = 1;
	  $writable{events_file} = 1;
	  $writable{email_reminders_datafile} = 1;
  
    my $calendars_table_exists=1;
    my $new_calendars_table_exists=1;
    my $events_table_exists=1;
      
      
    # if successful, check whether the calendars table exists  
    my $query_string="select * from $options{calendars_table} limit 0";
    my $sth = $dbh->prepare($query_string) || ($error_info .= "Can't prepare $statement: $dbh->errstr\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $calendars_table_exists=0;
      $error_info .= $dbh->errstr."\n";
    }
    $sth->finish();

    # check whether the new_calendars table exists  
    my $query_string="select * from $options{new_calendars_table} limit 0";
    my $sth = $dbh->prepare($query_string) || ($error_info .= "Can't prepare $statement: $dbh->errstr\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $new_calendars_table_exists=0;
      $error_info .= $dbh->errstr."\n";
    }
    $sth->finish();

    # check whether the events table exists  
    my $query_string="select * from $options{events_table} limit 0";
    my $sth = $dbh->prepare($query_string) || ($error_info .= "Can't prepare $statement: $dbh->errstr\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $events_table_exists=0;
      $error_info .= $dbh->errstr."\n";
    }
    $sth->finish();

    if ($events_table_exists + $new_calendars_table_exists + $calendars_table_exists == 3)
    {
      # everything's ok
    }
    elsif ($events_table_exists + $new_calendars_table_exists + $calendars_table_exists > 0)
    {
      $fatal_error = 1;
      $error_info .= "Ok, this is a serious problem.  Some of the required tables exist, but not all.\n  Plans can't fix this automatically.\n";
    }
    elsif ($events_table_exists + $new_calendars_table_exists + $calendars_table_exists == 0)
    {
      if ($q->param('create_tables') ne "1")
      {
        $fatal_error = 1;
        if ((-e "$options{calendars_file}") && (-e $options{new_calendars_file}) && (-e $options{events_file}))
        {    
          $error_info .= <<p1;
\nIt looks like the required tables don't exist. 
\nShall Plans create them for you?
\n<a href="$script_url/$name?create_tables=1">Yes, please create them (but don't import anything)</a>
\n<a href="$script_url/$name?create_tables=1&import_data=1">Yes, please create them, and import all all existing data from <b>$options{calendars_file}</b>, <b>$options{new_calendars_file}</b>, and <b>$options{events_file}</b>.</a>
p1
        }
        else
        {
          $error_info .= <<p1;
\nIt looks like the required tables don't exist.  
\nShall Plans create them for you?
\n<a href="$script_url/$name?create_tables=1">Yes, please create them</a>
p1
        }
      }
      else  # create the tables!
      {
        $error_info .= "\nCreating calendar and event tables...\n";
        # create the calendars table
        my $query_string="create table $options{calendars_table}(id int(5),xml_data text,update_timestamp int(15));";
        my $sth = $dbh->prepare($query_string) || ($error_info .= "Can't prepare $statement: $dbh->errstr\n");
        my $rv = $sth->execute();
        if ($dbh->errstr ne "")
        {
          $fatal_error = 1;
          $error_info .= "error creating table \"$options{calendars_table}\"!\n".$dbh->errstr."\n";
        }
        $sth->finish();
        
        # create the new calendars table
        my $query_string="create table $options{new_calendars_table}(id int(5),xml_data text,update_timestamp int(15));";
        my $sth = $dbh->prepare($query_string) || ($error_info .= "Can't prepare $statement: $dbh->errstr\n");
        my $rv = $sth->execute();
        if ($dbh->errstr ne "")
        {
          $fatal_error = 1;
          $error_info .= "error creating table \"$options{new_calendars_table}\"!\n".$dbh->errstr."\n";
        }
        $sth->finish();
      
        # create the events table
        my $query_string="create table $options{events_table}(id int(5),cal_ids text,start int(15),end int(15),xml_data text,update_timestamp int(15));";
        my $sth = $dbh->prepare($query_string) || ($error_info .= "Can't prepare $statement: $dbh->errstr\n");
        my $rv = $sth->execute();
        if ($dbh->errstr ne "")
        {
          $fatal_error = 1;
          $error_info .= "error creating table \"$options{events_table}\"!\n".$dbh->errstr."\n";
        }
        $sth->finish();
        
        # either import existing text data, or create a record for the primary calendar
        if ($q->param('import_data') ne "1"  && $fatal_error != 1)  # create primary calendar
        {
          $error_info .= "\nAdding primary calendar...\n";

          $fatal_error = 0;
          # data for the primary calendar   
          my %primary_cal = %default_cal;
          $primary_cal{id}=0;
          $primary_cal{title}="Main Calendar";
          $primary_cal{password}=crypt("12345", substr("12345",0,2)); 
          $primary_cal{details}=<<p1;
This is the primary calendar.  You can't delete it (you can only rename it).  
The password for this calendar is "12345", which you should change right away.
This calendar's password is the "master password", and can be used to override 
the password of any other calendar. 
p1
          $primary_cal{update_timestamp}=time();
          
          
          my $cal_xml = &calendar2xml(\%primary_cal);
          
          # add the primary calendar to the table
          my $query_string="insert into $options{calendars_table} (id, xml_data, update_timestamp) values ($primary_cal{id}, '$cal_xml', $primary_cal{update_timestamp});";
          my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
          my $rv = $sth->execute();
          if ($dbh->errstr ne "")
          {
            $fatal_error = 1;
            $error_info .= "Error adding primary calendar!\n".$dbh->errstr."\n";
            $error_info .= "$query_string\n";
          }
          else
          {
            $fatal_error = 1;
            $error_info = <<p1;
Tables created!<br/>
(you shouldn't ever see this message again.  To prove it, refresh the page or <a href="$script_url/$name">click here</a>.)
p1
          }
          $sth->finish();
        }
        else  # import data
        {
          $error_info .= "\nImporting data from flat files...\n";
          my $temp = $options{data_storage_mode};
          
          $options{data_storage_mode} = 0;
          &load_calendars();
          &load_new_calendars();
          &load_events("all");
          $options{data_storage_mode} = $temp;
          
          my @temp_cal_ids = keys %calendars;
          &add_calendars(\@temp_cal_ids);
          
          my @temp_new_cal_ids = keys %new_calendars;
          &add_new_calendars(\@temp_new_cal_ids);
          
          my @temp_event_ids = keys %events;
          &add_events(\@temp_event_ids);
          
          if ($dbh->errstr ne "")
          {
            $fatal_error = 1;
            $error_info .= "Error adding primary calendar!\n".$dbh->errstr."\n";
            $error_info .= "$query_string\n";
          }
          else
          {
            $fatal_error = 1;
            $error_info = <<p1;
Tables created, data imported!<br/>
(you shouldn't ever see this message again.  To prove it, refresh the page or <a href="$script_url/$name">click here</a>.)
p1
          }
        }
      }
    }
    #$dbh->disconnect;
  }
}

sub load_calendars
{
  my $max_update_timestamp=0;
  my $latest_cal_id=0;
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    open (FH, "$options{calendars_file}") || {$debug_info.= "unable to open file $options{calendars_file}\n"};
    flock FH,2;
    my @calendar_lines=<FH>;
    close FH;

    # For the calendars, we do "complete" xml parsing (no validation or DTD though)
    foreach $line (@calendar_lines)
    {
      if ($line !~ /\S/) {next;}          # ignore blank lines
      #if ($line =~ /<\/?xml>/) {next;}    # ignore <xml> and </xml lines>
      
      
      my %calendar = %{&xml2calendar($line)};
      $calendars{$calendar{id}} = \%calendar;
      
      #the calendar with id 0 is assumed to be the master calendar.
      #its password can be used to approve/edit/delete any event
      #for any calendar
      if ($calendar{id} eq "0")
        {$master_password = $calendar{password};}
        
      if ($calendar{update_timestamp} > $max_update_timestamp)
      {
        $max_update_timestamp = $calendar{update_timestamp};
        $latest_cal_id = $calendar{id};
      }
    }
    
    %latest_calendar = %{$calendars{$latest_cal_id}};
  }
  elsif ($options{data_storage_mode} == 1 ) # SQL database
  {
  
    my $query_string="select * from  $options{calendars_table};";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $debug_info .= "Error loading calendars!\n".$dbh->errstr."\n";
      $debug_info .= "query string:\n$query_string\n";
    }

    while(@row = $sth->fetchrow_array) 
    {
      my $cal_id = $row[0];
      my $line = $row[1];
      
      
      my %calendar = %{&xml2calendar($line)};
      $calendars{$calendar{id}} = \%calendar;

      #the calendar with id 0 is assumed to be the master calendar.
      #its password can be used to approve/edit/delete any event
      #for any calendar
      if ($calendar{id} eq "0")
        {$master_password = $calendar{password};}
        
      if ($calendar{update_timestamp} > $max_update_timestamp)
      {
        $max_update_timestamp = $calendar{update_timestamp};
        $latest_cal_id = $calendar{id};
      }
    }
    
    %latest_calendar = %{$calendars{$latest_cal_id}};
    $sth->finish();

  }
  
  # force all calendars to the same timezone?
  if ($options{force_single_timezone} eq "1")
  {
    foreach $cal_id (keys %calendars)
    {
      $calendars{$cal_id}{gmtime_diff} = $calendars{0}{gmtime_diff};
    }
  }
  
}

sub load_new_calendars()
{
  my $latest_new_cal_id=0;
  my $max_update_timestamp=0;
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    open (FH, "$options{new_calendars_file}") || {$debug_info.= "unable to open file $options{new_calendars_file}\n"};
    flock FH,2;
    my @calendar_lines=<FH>;
    close FH;

    # For the calendars, we do "complete" xml parsing (no validation or DTD though)
    foreach $line (@calendar_lines)
    {
      if ($line !~ /\S/) {next;}          # ignore blank lines

      my %calendar = %{&xml2calendar($line)};
      $new_calendars{$calendar{id}} = \%calendar;

      #the calendar with id 0 is assumed to be the master calendar.
      #its password can be used to approve/edit/delete any event
      #for any calendar
        
      if ($calendar{update_timestamp} > $max_update_timestamp)
      {
        $max_update_timestamp = $calendar{update_timestamp};
        $latest_new_cal_id = $calendar{id};
      }
    }
    %latest_new_calendar = %{$new_calendars{$latest_new_cal_id}};
  }
  elsif ($options{data_storage_mode} == 1 ) # SQL database
  {
  
    my $query_string="select * from  $options{new_calendars_table};";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $debug_info .= "Error loading new calendars!\n".$dbh->errstr."\n";
      $debug_info .= "query string:\n$query_string\n";
    }

    while(@row = $sth->fetchrow_array) 
    {
      my $cal_id = $row[0];
      my $line = $row[1];
      
      my %calendar = %{&xml2calendar($line)};
      $new_calendars{$calendar{id}} = \%calendar;

      #the calendar with id 0 is assumed to be the master calendar.
      #its password can be used to approve/edit/delete any event
      #for any calendar
        
      if ($calendar{update_timestamp} > $max_update_timestamp)
      {
        $max_update_timestamp = $calendar{update_timestamp};
        $latest_new_cal_id = $calendar{id};
      }
    }
    
    $sth->finish();
  }
  %latest_new_calendar = %{$new_calendars{$latest_new_cal_id}};
}




sub load_events()
{
  # load events for a given number of calendars, within a given time range.
  my ($start, $end, $temp) = @_;
  my @calendar_ids = @{$temp};

  #$debug_info .="start: $start\n";
  #$debug_info .="end: $end\n";

  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    open (FH, "$options{events_file}") || {$debug_info.= "unable to open file $options{events_file}\n"};
    flock FH,2;
    my @event_lines=<FH>;
    close FH;
    
    my $max_update_timestamp = 0;
    my $latest_event_id = 0;
    
    my $event_loaded_count = 0;
    foreach $line (@event_lines)
    {
      my $temp_line = substr($line,0,120);     # grab first 180 characters
      $temp_line =~ s/<title.+//;              # remove everything afer evt_label
      $temp_line =~ s/<event>//;               # remove <event>
      
      $temp_line =~ /<id>(\d+)/;
      my $evt_id = $1;
      
      
      my $temp_cal_ids;
      if ($temp_line =~ /<cal_id>(\d+)/)
      {
        $temp_cal_ids = $1;
      }
      elsif ($temp_line =~ /<cal_ids>(.+?)</)
      {
        $temp_cal_ids = $1;
      }
      
      $temp_line =~ /<start>(\d+)/;
      my $temp_start_timestamp = $1;
      $temp_line =~ /<end>(\d+)/;
      my $temp_end_timestamp = $1;
        
      my $cal_valid=0;
      foreach $cal_id (@calendar_ids)
      {
        if ($temp_cal_ids =~ /\b$cal_id\b/)
          {$cal_valid=1;}
      }
      if ($cal_valid == 0 && $start ne "all") {next;}   # event on some other calendar that we don't care about
      
      if ($temp_end_timestamp < $start && $start ne "all") {next;}  # in the past
      if ($temp_start_timestamp > $end && $start ne "all") {next;}  # in the future
      
      $event_loaded_count++;
      
      my %event = %{&xml2event($line)};
      $events{$event{id}} = \%event;
    
      if ($event{id} > $max_event_id)
        {$max_event_id = $event{id};}
        
      if ($event{series_id} > $max_series_id)
        {$max_series_id = $event{series_id};}
        
      if ($event{update_timestamp} > $max_update_timestamp)
      {
        $max_update_timestamp = $event{update_timestamp};
        $latest_event_id = $event{id};
      }
    }
    #$debug_info .= "$event_loaded_count events total.\n";
    #$debug_info .= "loaded event $evt_id\n";
          
    %latest_event = %{$events{$latest_event_id}};
  }
  elsif ($options{data_storage_mode} == 1 ) # SQL database
  {
    my $query_string;
    if ($start eq "all") 
    {
      $query_string="select * from $options{events_table};";
      $loaded_all_events = 1;
    }
    else
    {
      $query_string="select * from $options{events_table} where (start > $start and end < $end )";
    
      if ($calendar_ids[0] ne "" && $calendar_ids[0] !~ /\D/)
      {
        $query_string .= " and ( cal_ids='$calendar_ids[0]' or cal_ids like '$calendar_ids[0],' or cal_ids like '%,$calendar_ids[0]' or cal_ids like '%,$calendar_ids[0],%'";
        for ($l1=1;$l1<scalar @calendar_ids;$l1++)
        {
          if ($calendar_ids[$l1] ne "" && $calendar_ids[$l1] !~ /\D/)
            {$query_string .= " or cal_ids='$calendar_ids[$l1]' or cal_ids like '$calendar_ids[$l1],' or cal_ids like '%,$calendar_ids[$l1]' or cal_ids like '%,$calendar_ids[$l1],%'";}
        }
        $query_string .= ")";
      }
      
      $query_string .= ";";
    }
    
    
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $debug_info .= "Error loading events!\n".$dbh->errstr."\n";
      $debug_info .= "query string:\n$query_string\n";
    }

    while(@row = $sth->fetchrow_array) 
    {
      my $evt_id = $row[0];
      my $temp_cal_id = $row[1];
      #my $temp_start_timestamp = $row[2];
      #$temp_line =~ /<end>(\d+)/;
      #my $temp_end_timestamp = $row[3];
        
      my $cal_valid=0;
      foreach $cal_id (@calendar_ids)
      {
        if ($temp_cal_id == $cal_id)
          {$cal_valid=1;}
      }
      
      my $line = $row[4];
      my %event = %{&xml2event($line)};
      $events{$event{id}} = \%event;
      
      if ($event{series_id} > $max_series_id)
        {$max_series_id = $event{series_id};}
    }  
    # get max event id
    my $query_string="select max(id) from $options{events_table};";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $debug_info .= "$dbh->errstr\n";
      $debug_info .= "query string:\n$query_string\n";
    }
    $max_event_id = $sth->fetchrow_array;
    
    # get latest event
    my $query_string="select * from $options{events_table} order by update_timestamp desc limit 0, 1;";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $debug_info .= "$dbh->errstr\n";
      $debug_info .= "query string:\n$query_string\n";
    }
    while(@row = $sth->fetchrow_array) 
    {
      my $evt_id = $row[0];
      my $temp_cal_id = $row[1];
      
      my $line = $row[4];
      $line  =~ s/<\/?event>//g;      # remove <event> and </event>

      %latest_event = %{&xml2event($line)};
      $latest_event_id = $latest_event{id};
    }
  }
}

sub load_event()
{
  # load a single event.
  my ($event_id) = @_;

  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    open (FH, "$options{events_file}") || {$debug_info.= "unable to open file $options{events_file}\n"};
    flock FH,2;
    my @event_lines=<FH>;
    close FH;
    
    my $max_update_timestamp = 0;
    my $latest_event_id = 0;
    
    my $event_loaded_count = 0;
    foreach $line (@event_lines)
    {
      my $temp_line = substr($line,0,120);     # grab first 180 characters
      $temp_line =~ s/<title.+//;              # remove everything afer evt_label
      $temp_line =~ s/<event>//;               # remove <event>
      
      $temp_line =~ /<id>(\d+)/;
      my $evt_id = $1;
      
      if ($evt_id != $event_id) {next;}   # some other event that we don't care about      
      
      my %event = %{&xml2event($line)};
      $events{$event{id}} = \%event;
    
      if ($event{id} > $max_event_id)
        {$max_event_id = $event{id};}
      
      if ($event{update_timestamp} > $max_update_timestamp)
      {
        $max_update_timestamp = $event{update_timestamp};
        $latest_event_id = $event{id};
      }
      last;
    }
  }
  elsif ($options{data_storage_mode} == 1 ) # SQL database
  {
    my $query_string;
    $query_string="select * from $options{events_table} where (id = $event_id);";
    
    
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $debug_info .= "Error loading events!\n".$dbh->errstr."\n";
      $debug_info .= "query string:\n$query_string\n";
    }

    while(@row = $sth->fetchrow_array) 
    {
      #$debug_info .= "(load_event) loaded event $row[0]\n";
      my $evt_id = $row[0];
      my $temp_cal_ids = $row[1];
      
      my $line = $row[4];
      my %event = %{&xml2event($line)};
      $events{$event{id}} = \%event;
    }  
  }
}

sub load_remote_events()
{
  my ($remote_events_xml, $rcl) = @_;
  
  %remote_calendar_link=%{$rcl};
  
  
  my @remote_calendars = &xml_quick_extract($remote_events_xml, "calendar");

  foreach $temp (@remote_calendars)
  {
    my %remote_calendar = %{&xml2calendar($temp)};
    #$debug_info .= "successfully fetched remote calendar: $remote_calendar{title}\n";
  }  

  my @remote_events = &xml_quick_extract($remote_events_xml, "event");
  foreach $temp (@remote_events)
  {
    my %remote_event = %{&xml2event($temp)};
    #$debug_info .= "successfully fetched remote event: $remote_event{title}\n";
    
    my $new_remote_event_id = "r".($max_remote_event_id);
    $remote_event{remote_event_id} = $remote_event{id};
    $remote_event{id} = $new_remote_event_id;
    $remote_event{remote_calendar}=\%remote_calendar_link;
    
    #$debug_info .= "remote url: $remote_event{remote_calendar}{url}\n";
    #$debug_info .= "new remote id: $new_remote_event_id\n";
    
    $events{$new_remote_event_id} = \%remote_event;
    
    $max_remote_event_id++;
  }
  #$debug_info .= "event r2: $events{r2}{title} ($events{r2}{start})\n";
  #$debug_info .= "\n\n";
}



sub get_events_in_series()
{
  my ($series_id) = @_;
  my @series_ids=();
  
  &load_events("all") unless $loaded_all_events;
  foreach $event_id (keys %events)
  {
    if ($events{$event_id}{series_id} eq $series_id)
    {
      push @series_ids, $event_id
    }
  }

  return @series_ids;
}

# add an event to the data file
sub add_event()
{
  my ($event_id) = @_;
  # temporary copy of the event in question
  my %temp_event = %{$events{$event_id}};

  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text="";
    my $event_xml .= &event2xml($events{$event_id})."\n";
    $event_xml =~ s/(<update_timestamp>)\d*(<\/update_timestamp>)/$1$rightnow$2/;
    open (FH, ">>$options{events_file}") || {$debug_info .= "unable to open file $options{events_file} for writing!\n"};
    flock FH,2;
    print FH $event_xml;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    my $event_xml = &event2xml(\%temp_event);
  
    my $cal_ids_string = "";
    foreach $cal_id (@{$temp_event{cal_ids}})
    {
      $cal_ids_string .= "$cal_id";
      if ($cal_id ne @{$temp_event{cal_ids}}[-1])
      {
        $cal_ids_string .= ",";
      }
    }
    $cal_ids_string =~ s/,$//;
  
    my $query_string="insert into $options{events_table} (id, cal_ids, start, end, xml_data, update_timestamp) values ($temp_event{id}, '$cal_ids_string', $temp_event{start}, $temp_event{end}, '$event_xml', $temp_event{update_timestamp});";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $fatal_error = 1;
      $debug_info .= "Error adding event!\n".$dbh->errstr."\n";
      $debug_info .= "query string:\n$query_string\n";
    }
    $sth->finish();
  }
}

# add multiple events to the data file
sub add_events()
{
  my ($event_ids_ref) = @_;
  
  my @event_ids = @{$event_ids_ref};
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text="";
    foreach $id (sort {$a <=> $b} @event_ids)
      {$out_text .= &event2xml($events{$id})."\n";}
    
    open (FH, ">>$options{events_file}") || {$debug_info .= "unable to open file $options{events_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    foreach $id (@event_ids)
    {
      if ($id eq "") {next};
      my %temp_event = %{$events{$id}};
      my $event_xml = &event2xml(\%temp_event);
  
      my $cal_ids_string = "";
      foreach $cal_id (@{$temp_event{cal_ids}})
      {
        $cal_ids_string .= "$cal_id";
        if ($cal_id ne @{$temp_event{cal_ids}}[-1])
        {
          $cal_ids_string .= ",";
        }
      }
      $cal_ids_string =~ s/,$//;
      
      $debug_info .= "(add events) event $id cal_ids_string: $cal_ids_string\n";
  
      my $query_string = "insert into $options{events_table} (id, cal_ids, start, end, xml_data, update_timestamp) values ($temp_event{id}, '$cal_ids_string', $temp_event{start}, $temp_event{end}, '$event_xml', $temp_event{update_timestamp});";
      my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
      my $rv = $sth->execute();
      if ($dbh->errstr ne "")
      {
        $fatal_error = 1;
        $debug_info .= "Error adding event!\n($query_string)\n".$dbh->errstr."\n";
        $debug_info .= "query string:\n$query_string\n";
      }
      $sth->finish();
    }
  }
}

# update an event (already present in the data file)
sub update_event()
{
  my ($event_id) = @_;
  
  # temporary copy of the event in question
  my %temp_event = %{$events{$event_id}};
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text="";
    foreach $id (sort {$a <=> $b} keys %events)
    {
      if ($id =~ /\D/) {next};
      my $event_xml = &event2xml($events{$id})."\n";
      $out_text .= $event_xml;
      #if ($id eq $event_id)
      #  {$event_xml =~ s/(<update_timestamp>)\d*(<\/update_timestamp>)/$1$rightnow$2/;}
    }
    open (FH, ">$options{events_file}") || {$debug_info .= "unable to open file $options{events_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    my $event_xml = &event2xml(\%temp_event);
  
    my $cal_ids_string = "";
    foreach $cal_id (@{$temp_event{cal_ids}})
    {
      $cal_ids_string .= "$cal_id";
      if ($cal_id ne @{$temp_event{cal_ids}}[-1])
      {
        $cal_ids_string .= ",";
      }
    }
    $cal_ids_string =~ s/,$//;
  
    my $query_string="update $options{events_table} set cal_ids='$cal_ids_string', start=$temp_event{start}, end=$temp_event{end}, xml_data='$event_xml', update_timestamp=$temp_event{update_timestamp} where id=$temp_event{id};";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $fatal_error = 1;
      $debug_info .= "Error updating event!\n".$dbh->errstr."\n";
      $debug_info .= "query string:\n$query_string\n";
    }
    $sth->finish();
  }
}

# update multiple events
sub update_events()
{
  my ($event_ids_ref) = @_;
  my @event_ids = @{$event_ids_ref};
  
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text="";
    foreach $id (sort {$a <=> $b} keys %events)
    {
      if ($id =~ /\D/) {next};
      my $event_xml = &event2xml($events{$id})."\n";
      $out_text .= $event_xml;
      #if ($id eq $event_id)
      #  {$event_xml =~ s/(<update_timestamp>)\d*(<\/update_timestamp>)/$1$rightnow$2/;}
    }
    open (FH, ">$options{events_file}") || {$debug_info .= "unable to open file $options{events_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    # temporary copy of the event in question
    foreach $event_id (@event_ids)
    {   
      my %temp_event = %{$events{$event_id}};
      my $event_xml = &event2xml(\%temp_event);
  
      my $cal_ids_string = "";
      foreach $cal_id (@{$temp_event{cal_ids}})
      {
        $cal_ids_string .= "$cal_id";
        if ($cal_id ne @{$temp_event{cal_ids}}[-1])
        {
          $cal_ids_string .= ",";
        }
      }
      $cal_ids_string =~ s/,$//;
  
  
      my $query_string="update $options{events_table} set cal_ids='$cal_ids_string', start=$temp_event{start}, end=$temp_event{end}, xml_data='$event_xml', update_timestamp=$temp_event{update_timestamp} where id=$temp_event{id};";
      my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
      my $rv = $sth->execute();
      if ($dbh->errstr ne "")
      {
        $fatal_error = 1;
        $debug_info .= "Error updating event!\n".$dbh->errstr."\n";
        $debug_info .= "query string:\n$query_string\n";
      }
      $sth->finish();
    }
  }
}

# delete an event
sub delete_event()
{
  my ($event_id) = @_;
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    delete $events{$event_id};
    my $out_text="";
    foreach $id (sort {$a <=> $b} keys%events)
    {
      if ($id =~ /\D/) {next};
      $out_text .= &event2xml($events{$id})."\n";
    }
    open (FH, ">$options{events_file}") || {$debug_info .= "unable to open file $options{events_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    my $query_string="delete from $options{events_table} where id=$event_id;";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $fatal_error = 1;
      $debug_info .= "Error delteing event!\n".$dbh->errstr."\n";
      $debug_info .= "query string:\n$query_string\n";
    }
    $sth->finish();
  }
}

# delete multiple events
sub delete_events()
{
  my ($event_ids_ref) = @_;
  my @event_ids = @{$event_ids_ref};
  
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    foreach $event_id (@event_ids)
      {delete $events{$event_id};}

    my $out_text="";
    foreach $id (sort {$a <=> $b} keys%events)
    {
      if ($id =~ /\D/) {next};
      $out_text .= &event2xml($events{$id})."\n";
    }
    open (FH, ">$options{events_file}") || {$debug_info .= "unable to open file $options{events_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    foreach $event_id (@event_ids)
    {   
      my $query_string="delete from $options{events_table} where id=$event_id;";
      my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
      my $rv = $sth->execute();
      if ($dbh->errstr ne "")
      {
        $fatal_error = 1;
        $debug_info .= "Error deleting event!\n".$dbh->errstr."\n";
        $debug_info .= "query string:\n$query_string\n";
      }
      $sth->finish();
    }
  }
}

sub add_new_calendar()
{
  my ($cal_id) = @_;
  #$debug_info .= "adding new calendar $cal_id\n";
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    # write out the entire file!  Grossly inefficient, but that's how it goes if you don't use a DB.
    foreach $id (sort {$a <=> $b} keys %new_calendars)
    {
      #$debug_info .= "calendar2xml $id\n";
      my $cal_xml = &calendar2xml($new_calendars{$id})."\n";
      #if ($id eq $cal_id)
      #  {$cal_xml =~ s/(<update_timestamp>)\d*(<\/update_timestamp>)/$1$rightnow$2/;}
      $out_text .= $cal_xml;
    }

    open (FH, ">$options{new_calendars_file}") || {$debug_info.= "unable to open file $options{new_calendars_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;

  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    my $cal_xml = &calendar2xml($new_calendars{$cal_id})."\n";
    
    # add the primary calendar to the table
    my $query_string="insert into $options{new_calendars_table} (id, xml_data, update_timestamp) values ($cal_id, '$cal_xml', $new_calendars{$cal_id}{update_timestamp});";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $fatal_error = 1;
      $error_info .= "Error adding new calendar!\n".$dbh->errstr."\n";
      $error_info .= "$query_string\n";
    }
  }
}

sub add_new_calendars()  # add multiple calendars (this is used for data conversion)
{
  my ($add_new_cal_ids_ref) = @_;
  my @add_new_cal_ids = @{$add_new_cal_ids_ref};
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    foreach $new_cal_id (@add_new_cal_ids)
    {
      my $new_cal_xml = &calendar2xml($new_calendars{$new_cal_id});
      my $query_string="insert into $options{calendars_table} (id, xml_data, update_timestamp) values ($new_cal_id, '$new_cal_xml', $new_calendars{$new_cal_id}{update_timestamp});";
      my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
      my $rv = $sth->execute();
      if ($dbh->errstr ne "")
      {
        $debug_info .= "Error adding new calendar!\n".$dbh->errstr."\n";
        $debug_info .= "$query_string\n";
      }
    }
  }
}

sub delete_pending_calendars()  # this is called after a record is trandferred from new_calendars to calendars
{
  my ($temp1) = @_;
  
  my @pending_calendars_to_delete = @{$temp1};
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    # write out the entire file!  Grossly inefficient, but that's how it goes if you don't use a DB.
    foreach $calendar_id (sort {$a <=> $b} keys %new_calendars)
    {
      $out_text .= &calendar2xml($new_calendars{$calendar_id})."\n";
    }
    
    open (FH, ">$options{new_calendars_file}") || {$html_output.= "unable to open file $options{new_calendars_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    foreach $cal_id (@pending_calendars_to_delete)
    {
      my $query_string="delete from $options{new_calendars_table} where id=$cal_id;";
      my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
      my $rv = $sth->execute();
      if ($dbh->errstr ne "")
      {
        $debug_info .= "Error deleting pending calendar after approval!\n".$dbh->errstr."\n";
        $debug_info .= "$query_string\n";
      }
    }
  }
}

sub add_calendars()  # add multiple calendars (this is used for data conversion)
{
  my ($add_cal_ids_ref) = @_;
  my @add_cal_ids = @{$add_cal_ids_ref};
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text="";
    # write out the entire file!  Grossly inefficient, but that's how it goes if you don't use a DB.
    foreach $calendar_id (sort {$a <=> $b} keys %calendars)
    {
      my $cal_xml = &calendar2xml($calendars{$calendar_id})."\n";
      $out_text .= $cal_xml;
    }
    
    open (FH, ">$options{calendars_file}") || {$debug_info.= "unable to open file $options{calendars_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    foreach $cal_id (@add_cal_ids)
    {
      if ($cal_id eq "") {next};

      my $cal_xml = &calendar2xml($calendars{$cal_id});
      my $query_string="insert into $options{calendars_table} (id, xml_data, update_timestamp) values ($cal_id, '$cal_xml', $calendars{$cal_id}{update_timestamp});";
      my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
      my $rv = $sth->execute();
      if ($dbh->errstr ne "")
      {
        $debug_info .= "Error adding calendar!\n($query_string)\n".$dbh->errstr."\n";
        $debug_info .= "$query_string\n";
      }
    }
  }
}




sub update_calendar()
{
  my ($cal_id) = @_;
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text="";
    # write out the entire file!  Grossly inefficient, but that's how it goes if you don't use a DB.
    foreach $calendar_id (sort {$a <=> $b} keys %calendars)
    {
      my $cal_xml = &calendar2xml($calendars{$calendar_id})."\n";
      $out_text .= $cal_xml;
    }
    
    open (FH, ">$options{calendars_file}") || {$debug_info.= "unable to open file $options{calendars_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    my $cal_xml = &calendar2xml($calendars{$cal_id});
 
    # add the primary calendar to the table
    my $query_string="update $options{calendars_table} set xml_data='$cal_xml', update_timestamp=$calendars{$cal_id}{update_timestamp} where id=$cal_id;";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $fatal_error = 1;
      $error_info .= "Error updating calendar!\n".$dbh->errstr."\n";
      $error_info .= "$query_string\n";
    }
  }
}


sub update_calendars()  # update multiple calendars
{
  my ($update_cal_ids_ref) = @_;
  my @update_cal_ids = @{$update_cal_ids_ref};
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text="";
    # write out the entire file!  Grossly inefficient, but that's how it goes if you don't use a DB.
    foreach $calendar_id (sort {$a <=> $b} keys %calendars)
    {
      my $cal_xml = &calendar2xml($calendars{$calendar_id})."\n";
      $out_text .= $cal_xml;
    }
    
    open (FH, ">$options{calendars_file}") || {$debug_info.= "unable to open file $options{calendars_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {
    foreach $cal_id (@update_cal_ids)
    {
      my $cal_xml = &calendar2xml($calendars{$cal_id});
      my $query_string="update $options{calendars_table} set xml_data='$cal_xml', update_timestamp=$calendars{$cal_id}{update_timestamp} where id=$cal_id;";
      my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
      my $rv = $sth->execute();
      if ($dbh->errstr ne "")
      {
        $debug_info .= "Error updating calendar!\n".$dbh->errstr."\n";
        $debug_info .= "$query_string\n";
      }
    }
  }
}


sub delete_calendar()
{
  my ($cal_id) = @_;
  
  delete $calendars{$cal_id};
  
  if ($options{data_storage_mode} == 0 )  # flat text files
  {
    my $out_text ="";
    # write out the entire file!  Grossly inefficient, but that's how it goes if you don't use a DB.
    foreach $calendar_id (sort {$a <=> $b} keys %calendars)
    {
      my $cal_xml = &calendar2xml($calendars{$calendar_id})."\n";
      $out_text .= $cal_xml;
    }
    
    open (FH, ">$options{calendars_file}") || {$debug_info.= "unable to open file $options{calendars_file} for writing!\n"};
    flock FH,2;
    print FH $out_text;
    close FH;
  }
  elsif ($options{data_storage_mode} == 1 )  # DBI
  {      
    my $query_string="delete from $options{calendars_table} where id=$cal_id;";
    my $sth = $dbh->prepare($query_string) or ($error_info .= "Can't prepare $query_string:\n");
    my $rv = $sth->execute();
    if ($dbh->errstr ne "")
    {
      $debug_info .= "Error deleting calendar!\n".$dbh->errstr."\n";
      $debug_info .= "$query_string\n";
    }

  }
}



sub calendar2xml()
{
   my ($calendar_ref) = @_;
   
   my %calendar = %{$calendar_ref};
   
   #$error_info .= "Calendar title: $calendar{title}\n";
   
   my $xml_data = "<calendar>";
   $xml_data .= &xml_store($calendar{id}, "id");
   $xml_data .= &xml_store($calendar{title}, "title");
   $xml_data .= &xml_store($calendar{details}, "details");
   $xml_data .= &xml_store($calendar{link}, "link");
   $xml_data .= &xml_store($calendar{password}, "admin_password");
   
  # add local background calendars            
  foreach $local_background_calendar_id (sort {$a <=> $b} keys %{$calendar{local_background_calendars}})
    {$xml_data .= "<background_calendar><id>$local_background_calendar_id</id></background_calendar>";}


  # add remote background calendars
  #$debug_info .= "adding remote background calendars\n";       
  foreach $remote_background_calendar_id (sort {$a <=> $b} keys %{$calendar{remote_background_calendars}})
  {
    #$debug_info .= "&nbsp id $remote_background_calendar_id\n";       
    my %c = %{$calendar{remote_background_calendars}{$remote_background_calendar_id}};
    if (lc $c{type} eq "plans")
    {
      #$debug_info .= "  type: $c{type}\n";       
      $xml_data .= "<remote_background_calendar><id>$remote_background_calendar_id</id><type>$c{type}</type><version>$c{version}</version><remote_id>$c{remote_id}</remote_id><url>$c{url}</url><password>$c{password}</password></remote_background_calendar>";
    }
  }
    
  # add selectable calendars            
  foreach $selectable_calendar (sort {$a <=> $b} keys %{$calendar{selectable_calendars}})
    {$xml_data .= "<selectable_calendar>$selectable_calendar</selectable_calendar>";}
    
  # add other fields            
  $xml_data .= &xml_store($calendar{new_calendars_automatically_selectable}, "new_calendars_automatically_selectable");
  $xml_data .= &xml_store($calendar{list_background_calendars_together}, "list_background_calendars_together");
  $xml_data .= &xml_store($calendar{calendar_events_color}, "calendar_events_color");
  $xml_data .= &xml_store($calendar{background_events_display_style}, "background_events_display_style");
  $xml_data .= &xml_store($calendar{background_events_fade_factor}, "background_events_fade_factor");
  $xml_data .= &xml_store($calendar{background_events_color}, "background_events_color");
  $xml_data .= &xml_store($calendar{default_number_of_months}, "default_number_of_months");
  $xml_data .= &xml_store($calendar{max_number_of_months}, "max_number_of_months");
  $xml_data .= &xml_store($calendar{gmtime_diff}, "gmtime_diff");
  $xml_data .= &xml_store($calendar{date_format}, "date_format");
  $xml_data .= &xml_store($calendar{week_start_day}, "week_start_day");
  $xml_data .= &xml_store($calendar{preload_event_details}, "preload_event_details");
  $xml_data .= &xml_store($calendar{info_window_size}, "info_window_size");
  $xml_data .= &xml_store($calendar{custom_template}, "custom_template");
  $xml_data .= &xml_store($calendar{custom_stylesheet}, "custom_stylesheet");
  $xml_data .= &xml_store($calendar{update_timestamp}, "update_timestamp");
  $xml_data .= &xml_store($calendar{allow_remote_calendar_requests}, "allow_remote_calendar_requests");
  $xml_data .= &xml_store($calendar{remote_calendar_requests_require_password}, "remote_calendar_requests_require_password");
  $xml_data .= &xml_store($calendar{remote_calendar_requests_password}, "remote_calendar_requests_password");
  
  $xml_data .= "</calendar>";

  return $xml_data;
}


sub xml2event()
{
  my ($xml) = @_;
  my $event;
    
  $xml  =~ s/<\/?event>//g;      # remove <event> and </event>
  
  my ($id) = &xml_quick_extract($xml, "id");
  $id = &decode($id);

  my ($cal_id) = &xml_quick_extract($xml, "cal_id");
  $cal_id = &decode($cal_id);
  
  my ($cal_ids) = &xml_quick_extract($xml, "cal_ids");
  $cal_ids = &decode($cal_ids);
  
  my ($evt_start) = &xml_quick_extract($xml, "start");
  
  my ($evt_end) = &xml_quick_extract($xml, "end");
  
  my ($series_id) = &xml_quick_extract($xml, "series_id");
  $series_id = &decode($series_id);

  my ($evt_title) = &xml_quick_extract($xml, "title");
  $evt_title = &decode($evt_title);
  
  my ($evt_details) = &xml_quick_extract($xml, "details");
  $evt_details = &decode($evt_details);
  
  my $details_url = "";
  if ($evt_details =~ /^http.*:\/\/.+\s*$/)
    {$details_url=1;}
  
  
  my ($evt_icon) = &xml_quick_extract($xml, "icon");
  $evt_icon = &decode($evt_icon);
  
  my ($evt_bgcolor) = &xml_quick_extract($xml, "bgcolor");
  $evt_bgcolor = &decode($evt_bgcolor);
  
  my ($evt_unit_number) = &xml_quick_extract($xml, "unit_number");
  $evt_unit_number = &decode($evt_unit_number);
  
  my $update_timestamp = 0;
  ($update_timestamp) = &xml_quick_extract($xml, "update_timestamp");

  my $evt_days = int(($evt_end - $evt_start)/86400)+1;
  
  my $event_duration = $evt_end-$evt_start;
  #$debug_info .= "event $id ($evt_title) duration: ($event_duration)\n";
  
  # create cal_ids hash
  my @cal_ids_array;
  
  if ($cal_id ne "")
  {
    push @cal_ids_array, $cal_id;
  }
  else
  {
    @cal_ids_array = split(',', $cal_ids);
  }
  
  
  my $all_day_event = "";
  my $no_end_time = "";
  if (($event_duration+1) % 86400 == 0)
  {
    $all_day_event = 1;
    #$debug_info .= "event $id ($evt_title) is an all day event\n";
  }
  else
  {
    if ($event_duration == 1)
    {
      $no_end_time = 1;
    }
    #$debug_info .= "event $id ($evt_title) is an all day event\n";
    # offset start and end by calendar offset.
    #my $timezone_offset = $calendars{$cal_ids_array[0]}{gmtime_diff} - $current_calendar{gmtime_diff};
  
    if ($current_cal_id eq "")
    {
      $current_cal_id = $cal_ids_array[0];
      %current_calendar = %{$calendars{$current_cal_id}};
    }
  
    my $timezone_offset = $calendars{$cal_ids_array[0]}{gmtime_diff};
    #$debug_info .= "(xml2event) event $id, timezone offset: $timezone_offset\n";
  
    $evt_start += $timezone_offset * 3600;
    $evt_end += $timezone_offset * 3600;
    #$debug_info .= "(xml2event) event $id start: $evt_start\n";
    
    # experimental--may cause problems.
    # used to stretch an event that crosses midnight over 2 days.
    my @temp1=gmtime $evt_start;
    my @temp2=gmtime $evt_end;
    
    if ($temp1[3] != $temp2[3])
    {
      $evt_days++;
    }
  
  }
  #$debug_info .= "loaded event $id\n";
  #$debug_info .= "current calendar: $current_calendar{id}\n";
  #$debug_info .= "gmtime diff: $current_calendar{gmtime_diff}\n";

  $event = {id => $id, 
               cal_ids => \@cal_ids_array, 
               start => $evt_start, 
               end => $evt_end, 
               days => $evt_days,
               series_id => $series_id, 
               all_day_event => $all_day_event,
               no_end_time => $no_end_time,
               details_url => $details_url,
               title => $evt_title, 
               details => $evt_details,
               icon => $evt_icon,
               bgcolor => $evt_bgcolor,
               unit_number => $evt_unit_number,
               update_timestamp => $update_timestamp};
  
  return $event;

}



sub xml2calendar()
{
  my ($xml) = @_;
  
  my $calendar;
  
  $xml =~ s/<\/?calendar>//g;      # remove <calendar> and </calendar>
  
  my ($cal_id) = &xml_quick_extract($xml, "id");
  
  my ($cal_title) = &xml_quick_extract($xml, "title");
  $cal_title = &decode($cal_title);
  
  my ($cal_details) = &xml_quick_extract($xml, "details");
  $cal_details = &decode($cal_details);
  
  my ($cal_link) = &xml_quick_extract($xml, "link");
  $cal_link = &decode($cal_link);
  
  my ($cal_password) = &xml_quick_extract($xml, "admin_password");
  $cal_password = &decode($cal_password);
  
  my $update_timestamp=0;
  ($update_timestamp) = &xml_quick_extract($xml, "update_timestamp");
  $update_timestamp = 0 if ($update_timestamp eq "");
  
  # extract local background calendars
  my @temp = &xml_quick_extract($xml, "background_calendar");
  my %local_background_calendars;
  my $num_background_calendars = scalar @temp;
  foreach $background_calendar (@temp)
  {
    my ($id) = &xml_quick_extract($background_calendar, "id");
    $local_background_calendars{$id} = 1;
  }
  
  # extract remote background calendars
  my @temp = &xml_quick_extract($xml, "remote_background_calendar");
  my %remote_background_calendars;
  my $num_remote_background_calendars = scalar @temp;
  foreach $remote_background_calendar (@temp)
  {
    my ($id) = &xml_quick_extract($remote_background_calendar, "id");
    my ($type) = &xml_quick_extract($remote_background_calendar, "type");
    my ($version) = &xml_quick_extract($remote_background_calendar, "version");
    my ($remote_id) = &xml_quick_extract($remote_background_calendar, "remote_id");
    my ($url) = &xml_quick_extract($remote_background_calendar, "url");
    my ($password) = &xml_quick_extract($remote_background_calendar, "password");
    
    $remote_background_calendars{$id} = {id => $id, 
                                        type => $type, 
                                        version => $version, 
                                        remote_id => $remote_id, 
                                        url => $url,
                                        password => $password}
  }
  
  # extract selectable calendars
  my %selectable_calendars;
  @temp = &xml_quick_extract($xml, "selectable_calendar");
  foreach $selectable_calendar (@temp)
  {
    $selectable_calendars{$selectable_calendar} = 1;
  }
  
  my ($new_calendars_automatically_selectable) = &xml_quick_extract($xml, "new_calendars_automatically_selectable");
  $new_calendars_automatically_selectable = "no" if ($new_calendars_automatically_selectable eq "");
  
  my ($list_background_calendars_together) = &xml_quick_extract($xml, "list_background_calendars_together");
  $list_background_calendars_together = "no" if ($list_background_calendars_together eq "");

  my ($calendar_events_color) = &xml_quick_extract($xml, "calendar_events_color");
  $calendar_events_color = &decode($calendar_events_color);
  
  my ($background_events_display_style) = &xml_quick_extract($xml, "background_events_display_style");
  $background_events_display_style = "normal" if ($background_events_display_style eq "");
  
  my ($background_events_fade_factor) = &xml_quick_extract($xml, "background_events_fade_factor");
  $background_events_fade_factor = 1 if ($background_events_fade_factor eq "" || $background_events_fade_factor < 1);
  
  my ($background_events_color) = &xml_quick_extract($xml, "background_events_color");
  $background_events_color = &decode($background_events_color);
  $background_events_color = "#ffffff" if ($background_events_color eq "");

  my ($default_number_of_months) = &xml_quick_extract($xml, "default_number_of_months");
  $default_number_of_months = 1 if ($default_number_of_months eq "");
   
  my ($max_number_of_months) = &xml_quick_extract($xml, "max_number_of_months");
  $max_number_of_months = 24 if ($max_number_of_months eq "");
  
  my ($gmtime_diff) = &xml_quick_extract($xml, "gmtime_diff");
  $gmtime_diff = &decode($gmtime_diff);
  $gmtime_diff = 0 if ($gmtime_diff eq "");
  
  my ($date_format) = &xml_quick_extract($xml, "date_format");
  $date_format = &decode($date_format);
  $date_format = "mm/dd/yy" if ($date_format eq "");
  
  my ($week_start_day) = &xml_quick_extract($xml, "week_start_day");
  $week_start_day = "0" if ($week_start_day eq "");
  
  my ($preload_event_details) = &xml_quick_extract($xml, "preload_event_details");
  $preload_event_details = "no" if ($preload_event_details eq "");
        
  my ($info_window_size) = &xml_quick_extract($xml, "info_window_size");
  $info_window_size = "400x400" if ($info_window_size eq "");
  
  my ($custom_template) = &xml_quick_extract($xml, "custom_template");
  $custom_template = &decode($custom_template);
  
  my ($custom_stylesheet) = &xml_quick_extract($xml, "custom_stylesheet");
  $custom_stylesheet = &decode($custom_stylesheet);
  
  
  my ($allow_remote_calendar_requests) = &xml_quick_extract($xml, "allow_remote_calendar_requests");
  $allow_remote_calendar_requests = &decode($allow_remote_calendar_requests);
  
  my ($remote_calendar_requests_require_password) = &xml_quick_extract($xml, "remote_calendar_requests_require_password");
  $remote_calendar_requests_require_password = &decode($remote_calendar_requests_require_password);

  my ($remote_calendar_requests_password) = &xml_quick_extract($xml, "remote_calendar_requests_password");
  $remote_calendar_requests_password = &decode($remote_calendar_requests_password);
  
  if ($cal_id > $max_cal_id)
  {
    $max_cal_id = $cal_id;
  }
  
  $calendar = {id => $cal_id, 
               title => $cal_title, 
               details => $cal_details,
               link => $cal_link,
               local_background_calendars => \%local_background_calendars,
               remote_background_calendars => \%remote_background_calendars,
               selectable_calendars => \%selectable_calendars,
               new_calendars_automatically_selectable => $options{new_calendars_automatically_selectable},
               calendar_events_color => $calendar_events_color,
               list_background_calendars_together => $list_background_calendars_together,
               background_events_display_style => $background_events_display_style,
               background_events_fade_factor => $background_events_fade_factor,
               background_events_color => $background_events_color,
               allow_remote_calendar_requests => $allow_remote_calendar_requests,
               remote_calendar_requests_require_password => $remote_calendar_requests_require_password,
               remote_calendar_requests_password => $remote_calendar_requests_password,
               default_number_of_months => $default_number_of_months,
               max_number_of_months => $max_number_of_months,
               gmtime_diff => $gmtime_diff,
               date_format => $date_format,
               week_start_day => $week_start_day,
               preload_event_details => $preload_event_details,
               info_window_size => $info_window_size,
               custom_template => $custom_template,
               custom_stylesheet => $custom_stylesheet,
               password => $cal_password,
               update_timestamp => $update_timestamp};
  return $calendar;
}


sub normalize_timezone()
{
  foreach $event_id (keys %events)
  {
    #$debug_info .= "normalizing event $event_id\n";
    my %event = %{$events{$event_id}};
    #$debug_info .= "start: $events{$event_id}{start} end: $events{$event_id}{end}\n";
    if ($event{cal_ids}[0] ne $current_cal_id)
    {
       $events{$event_id}{start} -= $calendars{$event{cal_ids}[0]}{gmtime_diff} * 3600;
       $events{$event_id}{start} += $calendars{$current_cal_id}{gmtime_diff} * 3600;
       
       $events{$event_id}{end} -= $calendars{$event{cal_ids}[0]}{gmtime_diff} * 3600;
       $events{$event_id}{end} += $calendars{$current_cal_id}{gmtime_diff} * 3600;
      #$debug_info .= "new start: $events{$event_id}{start} new end: $events{$event_id}{end}\n";
    }
  }
}




sub event2xml()
{
  my ($event_ref) = @_;
  my %event = %{$event_ref};

  my $xml_data = "<event>";
  $xml_data .= &xml_store($event{id}, "id");
  my $cal_ids_string = "";
  foreach $cal_id (@{$event{cal_ids}})
  {
    $cal_ids_string .= "$cal_id";
    if ($cal_id ne @{$event{cal_ids}}[-1])
    {
      $cal_ids_string .= ",";
    }
  }
  $cal_ids_string =~ s/,$//;
  
  my $event_start_timestamp = $event{start};
  my $event_end_timestamp = $event{end};
  
  if ($event{all_day_event} ne "1")
  {
    $event_start_timestamp -= $calendars{$event{cal_ids}[0]}{gmtime_diff} * 3600;
    $event_end_timestamp -= $calendars{$event{cal_ids}[0]}{gmtime_diff} * 3600;
  }
  
  $xml_data .= "<cal_ids>$cal_ids_string</cal_ids>";
  $xml_data .= &xml_store($event_start_timestamp, "start");
  $xml_data .= &xml_store($event_end_timestamp, "end");
  $xml_data .= &xml_store($event{series_id}, "series_id");
  $xml_data .= &xml_store($event{title}, "title");
  $xml_data .= &xml_store($event{details}, "details");
  $xml_data .= &xml_store($event{icon}, "icon");
  $xml_data .= &xml_store($event{bgcolor}, "bgcolor");
  $xml_data .= &xml_store($event{unit_number}, "unit_number");
  $xml_data .= &xml_store($event{update_timestamp}, "update_timestamp");
  $xml_data .= "</event>";
  return $xml_data;
}

sub event2ical()
{
  my ($event_ref) = @_;
  my %event = %{$event_ref};
  my $results;
  
  my $start_timestamp = $event{start} - $calendars{$event{cal_ids}[0]}{timezone_offset}*3600;
  my $end_timestamp = $event{end} - $calendars{$event{cal_ids}[0]}{timezone_offset}*3600;
  #my $start_timestamp = $event{start};
  #my $end_timestamp = $event{end};
  my $cal_name=$calendars{$event{cal_ids}[0]}{title};  # need to add titles for all other cal_ids

  my $dtstart_string = &outlook_date_time($event{start});
  my $dtend_string = &outlook_date_time($event{end});
  
  $cal_name=$calendars{$event{cal_ids}[0]}{title};

  # replace newlines with carraige-returns (otherwise two newlines in a row
  # causes errors.  Not sure whether this is an outlook error or an IE error.
  $event{details} =~ s/\n/\r/g;
  $event{title} =~ s/\n/\r/g;

  $results =<<p1;
BEGIN:VCALENDAR
PRODID:-//Plans//EN
VERSION:3.0
METHOD:PUBLISH
BEGIN:VEVENT
ORGANIZER:
DTSTART:$dtstart_string
DTEND:$dtend_string
TRANSP:OPAQUE
SEQUENCE:0
UID:
DTSTAMP:20020322T043444Z
DESCRIPTION:$event{details}
SUMMARY:$event{title} ($cal_name)
PRIORITY:5
CLASS:PUBLIC
END:VEVENT
END:VCALENDAR
p1

  return $results;

}



sub event2vcal()
{
  my ($event_ref) = @_;
  my %event = %{$event_ref};
  my $results;
  
  my $start_timestamp = $event{start};
  my $end_timestamp = $event{end};
  my $cal_name=$calendars{$event{cal_ids}[0]}{title};

  my $dtstart_string = &outlook_date_time($event{start});
  my $dtend_string = &outlook_date_time($event{end});
  
  $cal_name=$calendars{$event{cal_ids}[0]}{title};

  # replace newlines with carraige-returns (otherwise two newlines in a row
  # causes errors.  Not sure whether this is an outlook error or an IE error.
  $event{details} =~ s/\n/\r/g;
  $event{title} =~ s/\n/\r/g;

  $results =<<p1;
BEGIN:VCALENDAR
PRODID:-//Plans//EN
VERSION:1.0
METHOD:PUBLISH
BEGIN:VEVENT
DTSTART:$dtstart_string
DTEND:$dtend_string
TRANSP:OPAQUE
SEQUENCE:0
DTSTAMP:20020322T043444Z
DESCRIPTION:$event{details}
SUMMARY:$event{title} ($cal_name)
PRIORITY:5
CLASS:PUBLIC
RRULE:D$event{days} $dtend_string
END:VEVENT
END:VCALENDAR
p1

  return $results;

}

sub outlook_date_time
{
  my ($timestamp) = @_;
  
  my @timestamp_array = gmtime($timestamp);
  my $year_string = 1900 + $timestamp_array[5];
  
  my $month_string = $timestamp_array[4]+1;
  if ($month_string < 10)
    {$month_string="0".$month_string;}
  
  my $mday_string = $timestamp_array[3];
  if ($mday_string < 10)
    {$mday_string="0".$mday_string;}
    
  my $hour_string = $timestamp_array[2];
  if ($hour_string < 10)
    {$hour_string="0".$hour_string;}
    
  $hour_string="$timestamp_array[2]";
  $hour_string = "0$hour_string" if (length $hour_string == 1);
  $minute_string="$timestamp_array[1]";
  $minute_string = "0$minute_string" if (length $minute_string == 1);
  $second_string="$timestamp_array[0]";
  $second_string = "0$second_string" if (length $second_string == 1);
  
  my $dt_string="$year_string$month_string$mday_string";
  $dt_string .= "T".$hour_string.$minute_string.$second_string;
  #$dt_string .= "T".$hour_string.$minute_string.$second_string."Z";
  #$dt_string .= "T".$hour_string."0000";

  return $dt_string;
}

sub event2palmcsv()
{
  my ($event_ref) = @_;
  my %event = %{$event_ref};
  my $results;
  
  
  my $palm_begin = &formatted_time($event{start}, "yy mo md  hh:mm");
  my $palm_end = &formatted_time($event{end}, "yy mo md  hh:mm");
  
  if ($event{days} == 1)
  {
  $results .= "\"\"";     # category
  $results .= ",\"0\"";   # private
  $results .= ",\"$event{title}\"";   # description
  $results .= ",\"$event{details}\""; # note
  $results .= ",\"1\"";               # event
  $results .= ",\"$palm_begin\"";     # begin time
  $results .= ",\"$palm_end\"";       # end time
  $results .= ",\"\"";    # alarm
  $results .= ",\"\"";    # advance
  $results .= ",\"\"";    # advance units
  $results .= ",\"0\"";   # repeat type
  $results .= ",\"\"";    # repeat forever
  $results .= ",\"\"";    # repeat end
  $results .= ",\"\"";    # repeat freq.
  $results .= ",\"\"";    # repeat day.
  $results .= ",\"\"";    # repeat days.
  $results .= ",\"\"";    # week start.
  $results .= ",\"\"";    # number of exceptions.
  $results .= ",\"\"";    # exceptions
  }
  else # multi-day event.
  {
  $results .= "\"\"";     # category
  $results .= ",\"0\"";   # private
  $results .= ",\"$event{title}\"";   # description
  $results .= ",\"$event{details}\""; # note
  $results .= ",\"1\"";               # event
  $results .= ",\"$palm_begin\"";     # begin time
  $results .= ",\"$palm_begin\"";       # end time
  $results .= ",\"\"";    # alarm
  $results .= ",\"\"";    # advance
  $results .= ",\"\"";    # advance units
  $results .= ",\"1\"";   # repeat type
  $results .= ",\"\"";    # repeat forever
  $results .= ",\"$palm_end\"";    # repeat end
  $results .= ",\"1\"";    # repeat freq.
  $results .= ",\"\"";    # repeat day.
  $results .= ",\"\"";    # repeat days.
  $results .= ",\"\"";    # week start.
  $results .= ",\"\"";    # number of exceptions.
  $results .= ",\"\"";    # exceptions
  }
  
  
  $results =~ s/\n/ /g;
  
  return $results;
}




sub find_end_of_month
{
  my ($month, $year) = @_;

  my $next_month = $month+1;
  if ($next_month > 11) 
  {
    $next_month=0;
    $year++;
  }
  my $month_end_timestamp = timegm(0,0,0,1,$next_month,$year);
  
  return $month_end_timestamp;

}

sub xml_store
{
  my ($data_string, $tag_name) = @_;
  my $result_string = "";
  $data_string = &encode($data_string);
  my $result_string = "<$tag_name>$data_string</$tag_name>";
  return $result_string;
}

sub xml_quick_extract   # it doesn't get any dumber than this.  ignores attributes, element order, fooled by duplicate tag names at different depths.
{
  my ($data, $tag_name) = @_;
  my @results_array = ();
                           
  while  ($data =~ /<$tag_name>(.+?)<\/$tag_name>/gs)
  {
    push @results_array, $1;
  }
  return @results_array;
}

sub xml_extract    # Slow, but can handle attributes, element order, same tag names at different depths. Can't handle encodings, DTDs.
{
  my ($data, $tag_name, $debug) = @_;
  my @results_array = ();
  my $results = "";
  my $final_results = "";
  
  my $depth_count=0;
  my $start_index=0;
  my $end_index=0;
  my $match_index=0;

  my $attributes=();
  my $position=0;          # position is the position of the element we're looking for, with respect to all other elements
                           # under the parent element
                           
  while  ($data =~ /(<.*?>|<\/.*?>)/g)
  {
    my $match=$1;
    my $temp_index = $+[1];
    if ($match =~ /<$tag_name\b.*?>/ && $depth_count==0)  # the opening tag we're looking for
    {
      $start_index = $temp_index;
      $depth_count++;
      if ($debug) {$debug_info .= "active opening tag, $match \ndepth count $depth_count\n";}
      if ($debug) {$debug_info .= "start index $start_index\n";}
      
      my $attribute_text = $match;
      $attribute_text =~ s/\s*=\s*/=/g;                # compress whitespace on either side of = sign
      $attribute_text =~ s/=([^"])(.+?\b)/="$1$2"/g;   # properly format attributes with quote marks
     
      #if ($debug) {$debug_info .= "rejiggered attribute text: $attribute_text\n";}
      
      # extract attributes
      while ($attribute_text =~ /\w+?=".+?[^\\]"/g)
      {
        my $a_match = $&;
        my ($name, $value)= split('=',$a_match);

        $value =~ s/\\"/"/g;
        # remove first and last characters (the quotes) from value
        $value = substr $value, 1,-1;
        
        if ($debug) {$debug_info .= "attribute: $a_match\n";}
        if ($debug) {$debug_info .= " name: $name\n";}
        if ($debug) {$debug_info .= " value: $value\n";}
        
        $attributes->{$name} = $value;
      }
      #%attributes=();
      #$debug_info .= "end position, $+[0]\n\n";
    }
    elsif ($match =~ /<[^\/].*?>/)  # some other opening tag
    {
      
      $depth_count++;
      if ($debug) {$debug_info .= "other opening tag, $match \ndepth count $depth_count\n";}
    }
    elsif ($match eq "<\/$tag_name>" && $depth_count == 1)  # the closing tag we're looking for
    {
      $depth_count--;
      if ($debug) {$debug_info .= "active closing tag, $match \ndepth count $depth_count\n";}
      if ($depth_count==0) # done!  return results
      {
        $end_index = $-[0];
        $results = substr $data, $start_index,($end_index-$start_index);
        
        my $results_hash=();
        $results_hash -> {data} = "".$results;
        $results_hash -> {attributes} = $attributes;
        $results_hash -> {position} = $position;
        push @results_array, $results_hash;
        
        if ($debug) {$debug_info .= "  pushing results: \"$results\" onto array\n";}
        if ($debug) {$debug_info .= "  attributes: \"$attributes\" \n";}
        if ($debug) {$debug_info .= "  position: \"$position\" \n";}
        if ($debug) {$debug_info .= "  start: $start_index end $end_index\n\n";}
        #if ($debug) {$debug_info .= " $results\n\n";}
        $start_index=0;
        $end_index=0;
        $attributes=();
      }
      #$debug_info .= "closing tag, $1 \ndepth count $depth_count\n";
      #$debug_info .= "start position, $-[0]\n\n";
      $position++;
    }
    else # other closing tag
    {
      $depth_count--;
      if ($depth_count==0)
      {$position++;}
      
      if ($debug) {$debug_info .= "other closing tag, $match \ndepth count $depth_count\n";}
    }
    $match_index++;
  }
  return @results_array;
}   #******************** end xml_extract **********************


sub xml_tags
{
  my ($data, $debug) = @_;
  my @results_array = ();
  my %tags_hash;
  
  my $depth_count=0;
                           
  while  ($data =~ /(<.*?>|<\/.*?>)/g)
  {
    my $match=$1;
    if ($match =~ /<[^\/].*?>/)  # any opening tag
    {
      if ($depth_count == 0)     # level 0 opening tag
      {
        $tag_name = $match;
        $tag_name =~ s/<//;
        $tag_name =~ s/\b(.+)\b.+/$1/;
        if ($debug) {$debug_info .= "level 0 opening tag, $tag_name \n\n";}
      }
      $depth_count++;

    }
    elsif ($depth_count == 1)  # level 1 closing tag
    {
      $tag_name = $match;
      $tag_name =~ s/<//;
      $tag_name =~ s/\/(.+)(\b|>).+/$1/;

      $depth_count--;
      if ($debug) {$debug_info .= "level 1 closing tag, $tag_name \n";}
      $tags_hash{$tag_name}=1;
        
      if ($debug) {$debug_info .= "  pushing tag name $tag_name onto array\n\n";}
    }
    else # other closing tag
    {
      $depth_count--;
      
      if ($debug) {$debug_info .= "other closing tag, $match \ndepth count $depth_count\n";}
    }
  }
  return keys %tags_hash;
  
}  #******************** end xml_tags **********************


sub xml2hash
{
  my ($xml_data, $debug) = @_;
  my $item;

  my @item_tags = &xml_tags($xml_data);
  
  if (scalar @item_tags == 0)
  {
    if ($debug) {$debug_info .= " plain text item data: ($xml_data) \n";}
    
    return $xml_data;
  }
  else
  {
    if ($debug) {$debug_info .= " xml data: ($xml_data) \n";}
    my %results_hash;
    foreach $tag (@item_tags)
    {
      my @tag_data = &xml_extract($xml_data,"$tag");
      
      if (scalar @tag_data == 1)
      {
        if ($debug) {$debug_info .= "  extracting xml for tag $tag (single data)\n";}
        $results_hash{$tag} = &xml2hash($tag_data[0]->{data},$debug);
      }
      else
      {
        if ($debug) {$debug_info .= "  extracting xml for tag $tag (array data)\n";}
        my @tag_array;
        foreach $thing (@tag_data)
        {
          push @tag_array, &xml2hash($thing->{data},$debug);
        }
        $results_hash{$tag}=\@tag_array;
      } 
    }
    if ($debug) {$debug_info .= "\n";}
    return \%results_hash;
  }
}  #******************** end xml2hash **********************

sub hash2xml {
  my ($temp, $parent_tag, $order_hashref) = @_;
  my $results="";
  
  my %order_hash = %{$order_hashref};
  
  #$debug_info .= "\nhash2xml: $temp, $parent_tag\n";
  
  if ($temp =~ /ARRAY\(0x/)  # array
  {
    my @temp_array = @{$temp};
    foreach $element (@temp_array)
    {
      $results .= "<$parent_tag>";
      $results .= $element;
      $results .= "</$parent_tag>";
    }
  }
  elsif ($temp =~ /HASH\(0x/) # hash
  {
    $results .= "<$parent_tag>";
    my %temp_hash = %{$temp};
    foreach $key (sort {$order_hash{$a} <=> $order_hash{$b}} keys %temp_hash)
    {
      $results .= &hash2xml($temp_hash{$key}, $key, $order_hashref);
    }
    $results .= "</$parent_tag>";
  }
  else # data
  {
    #$debug_info .= "hash2xml: data\n";
    $results .= "<$parent_tag>".&encode($temp)."</$parent_tag>";
  }
  
  return $results;
} #******************** end hash2xml **********************


sub get_remote_file
{
  my ($url) = @_;
  $url =~ s/http:\/\///;
  
  my $hostname = $url;
  $hostname =~ s/\/.+//g;

  my $document = $url;
  $document =~ s/.+?\//\//;
  
  #$debug_info .= "url: $url<br>";
  #$debug_info .= "hostname: $hostname<br>";
  #$debug_info .= "document: $document<br>";
    
  if ($hostname eq "" | $document eq "") {return;}

  $remote = IO::Socket::INET->new( Proto     => "tcp",
                                   PeerAddr  => $hostname,
                                   PeerPort  => "http(80)"
                                 );
  unless ($remote) 
  {
    $debug_info .= "cannot connect to http daemon on $hostname <br>";
    return;
  }
  $remote->autoflush(1);
  print $remote "GET $document HTTP/1.0\r\n";
  print $remote "User-Agent: Mozilla 4.0 (compatible; I; Linux-2.0.35i586)\r\n";
  print $remote "Host: $hostname\r\n"; #without this line, virtual hosts won't work (multiple domain names on a single IP)
  
  print $remote "\r\n\r\n";

  @textbuffer=<$remote>;
  my $textstring = join "", @textbuffer;
  
  $textstring =~ s/\r//gs;         #some servers sneak these in.
  $textstring =~ s/.+?\n\n//si;
  return $textstring;
}

sub time_overlap
{
  my ($start1, $end1, $start2, $end2) = @_;
  
  my $temp1 = $end2 - $start1;
  my $temp2 = $end1 - $start2;
  
  my $range_total = $end2 - $start2;
  
  #$debug_info .= "temp1:$temp1 temp2:$temp2 range_total:$range_total\n";
  
  
  # if the event falls in or overlaps this week (there are 3 cases), the third being an event
  # that *completely* overlaps the week.
  if ( ($temp1 <= $range_total && $temp1 > 0)  || ($temp2 <= $range_total && $temp2 > 0) || ($temp1 > 0 && $temp2 > 0))
    {return 1;}
  else 
    {return 0;}
}

sub make_email_link
{
  my ($string) = @_;
  my $new_string = "";
  #remove all newlines
  $string =~ s/\n//g;
  
  #insert newlines after > characters
  $string =~ s/</\n</g;
  
  my @lines = split ("\n", $string);
  
  foreach $line (@lines)
  {
    $line .= "\n";
    my $new_line = $line;
    $new_line =~ s/([^ >]+?\@[^ <>]+)/<a href=\"mailto:$1\">$1<\/a>/g;
    
    #ignore substitution if the email address was already a link.
    if ($1 =~ /(:|")/)
      {$new_string .= $line;}
    else
    {
      $new_line =~ s/\n//g;
      $new_string .= $new_line;
    }
  }
  return $new_string;  
}

sub formatted_time
{

  my ($input_time, $format_string) = @_;
  my @input_time_array = gmtime ($input_time+0);
  my $ampm = $lang{pm};
  
  if ($input_time_array[5]<1900) {$input_time_array[5]+=1900;}
  $month_name=$months[$input_time_array[4]];
  $input_time_array[4]++;

  if ($input_time_array[1]<10) {$input_time_array[1]="0".$input_time_array[1];}

  if ($input_time_array[2] < 12)
  {
    #$debug_info .= "$input_time -> $input_time_array[2] (am)\n";
    $ampm = $lang{am};
  }
  
  if(!$options{twentyfour_hour_format}) 
  {
    if ($input_time_array[2] > 12)  #convert from 24-hour to am/pm
    {
       $input_time_array[2] = $input_time_array[2] - 12;
    }
 
    if ($input_time_array[2] == 0)  #convert from 24-hour to am/pm
    {
       $input_time_array[2] = 12;
    }
  }
  else
  {
    $format_string =~ s/ampm//g;  
  }
  
  $format_string =~ s/ampm/$ampm/g;  
  $format_string =~ s/hh/$input_time_array[2]/g;
  $format_string =~ s/mm/$input_time_array[1]/g;  
  $format_string =~ s/ss/$input_time_array[0]/g;
  $format_string =~ s/mo/$input_time_array[4]/g;
  $format_string =~ s/mn/$month_name/g;
  $format_string =~ s/md/$input_time_array[3]/g;
  $format_string =~ s/yy/$input_time_array[5]/g;
  return $format_string;
}

sub nice_date_range_format
{
  my ($timestamp1, $timestamp2, $separator_string) = @_;
  my $result_string = "";

  #make sure the timestamps are in the correct order
  if ($timestamp1 > $timestamp2)
  {
    $temp=$timestamp2;
    $timestamp2=$timestamp1;
    $timestamp1=$temp;
  }
  
  my @timestamp1_array = gmtime $timestamp1;
  my @timestamp2_array = gmtime $timestamp2;
  
  #format the year for humans
  $timestamp1_array[5] +=1900;
  $timestamp2_array[5] +=1900;
  
  if (lc $current_calendar{date_format} eq "dd/mm/yy")
  {
    if ($timestamp1_array[4] == $timestamp2_array[4] && $timestamp1_array[5] == $timestamp2_array[5] && $timestamp1_array[3] == $timestamp2_array[3])
    { #same year, same month, same day
      $result_string = " $timestamp1_array[3] $months[$timestamp1_array[4]], $timestamp1_array[5]";
    }
    elsif ($timestamp1_array[4] == $timestamp2_array[4] && $timestamp1_array[5] == $timestamp2_array[5])
    { #same year, same month
      $result_string = "$timestamp1_array[3]$separator_string$timestamp2_array[3] $months[$timestamp1_array[4]], $timestamp1_array[5]";
    }
    elsif ($timestamp1_array[5] != $timestamp2_array[5])
    { #different year
      $result_string = "$timestamp1_array[3] $months[$timestamp1_array[4]], $timestamp1_array[5]$separator_string$timestamp2_array[3] $months[$timestamp2_array[4]], $timestamp2_array[5]";
    }
    else 
    { #same year, different months
      $result_string = "$timestamp1_array[3] $months[$timestamp1_array[4]]$separator_string$timestamp2_array[3] $months[$timestamp2_array[4]], $timestamp2_array[5]";
    }
  }
  #elsif (lc $current_calendar{date_format} eq "mm/dd/yy")
  else
  {
    if ($timestamp1_array[4] == $timestamp2_array[4] && $timestamp1_array[5] == $timestamp2_array[5] && $timestamp1_array[3] == $timestamp2_array[3])
    { #same year, same month, same day
      $result_string = "$months[$timestamp1_array[4]] $timestamp1_array[3], $timestamp1_array[5]";
    }
    elsif ($timestamp1_array[4] == $timestamp2_array[4] && $timestamp1_array[5] == $timestamp2_array[5])
    { #same year, same month
      $result_string = "$months[$timestamp1_array[4]] $timestamp1_array[3]$separator_string$timestamp2_array[3], $timestamp1_array[5]";
    }
    elsif ($timestamp1_array[5] != $timestamp2_array[5])
    { #different year
      $result_string = "$months[$timestamp1_array[4]] $timestamp1_array[3], $timestamp1_array[5]$separator_string$months[$timestamp2_array[4]] $timestamp2_array[3], $timestamp2_array[5]";
    }
    else 
    { #same year, different months
      $result_string = "$months[$timestamp1_array[4]] $timestamp1_array[3]$separator_string$months[$timestamp2_array[4]] $timestamp2_array[3], $timestamp2_array[5]";
    }
  }
  
  return $result_string;
}

sub nice_time_range_format
{
  my ($start, $end) = @_;
  my $results = "";
  $results = &formatted_time($start,"hh:mm ampm")." - ".&formatted_time($end,"hh:mm ampm");
  
  # if times are the same, remove the second one.
  if ($end - $start <=1)
  {
    $results =~ s/s*-.+//;
    return $results;
  }
  
  # if both times are am or pm, remove the first one (it's redundant!)
  $results =~ s/(.*) $lang{am}(.*$lang{am}.*)/$1$2/;
  $results =~ s/(.*) $lang{pm}(.*$lang{pm}.*)/$1$2/;
  return $results;
}


sub encode
{
  my ($input_string) = @_;
  return if ($input_string eq "");
  my $output_string=$input_string;

  $output_string =~ s/(\W)/"\%".sprintf("%02x", (ord $1))/ge;
  $output_string =~ s/\%20/+/g;
  return $output_string;
}

sub decode
{
  my ($input_string) = @_;
  return if ($input_string eq "");
  my $output_string = $input_string;
  
  $output_string =~ s/\+/ /g;
  $output_string =~ s/%([0-9A-Fa-f]{2})/pack("c",hex($1))/ge;
  return $output_string;
}

sub min { @_ = sort {$a <=> $b} @_; shift; }
sub max { @_ = sort {$a <=> $b} @_; pop; }

sub generate_event_details
{
  my ($event_ref, $preview) = @_;
  
  my %event = %{$event_ref};

  my $return_text = $event_details_template;
  my @event_start_timestamp_array = gmtime $event{start};
  
  my $event_cal_title_text = "";
  foreach $temp_cal_id (@{$event{cal_ids}})
  {
    my $event_cal_name = "$calendars{$temp_cal_id}{title}";
    if ($calendars{$temp_cal_id}{link} =~ /\S/)
    {
      $event_cal_name = "<a target= _blank href=\"http://$calendars{$temp_cal_id}{link}\">$calendars{$temp_cal_id}{title}</a>";
    }
    $event_cal_title_text .= $event_cal_name.",";
  }
  $event_cal_title_text =~ s/,$//; # remove trailing comma
  
  $return_text =~ s/###event calendar name###/$event_cal_title_text/g;
  
  my $date_string = $lang{event_details_date_goes_here};
  my $event_time = "";
  
  if ($event{start} ne "")
  {
    $date_string = &nice_date_range_format($event{start}, $event{end}, " - ");
    if ($event{all_day_event} ne "1")
    {
      $event_time = &nice_time_range_format($event{start},$event{end});
      # if both times are am or pm, remove the first one (it's redundant!)
      $event_time = "<span class=\"event_time\">$event_time</span>";
    }
  }
  
  $return_text =~ s/###event date###/$date_string/g;
  $return_text =~ s/###event time###/$event_time/g;
  
  
  $return_text =~ s/###event title###/$event{title}/g;
  $return_text =~ s/###event id###/$event{id}/g;
  $return_text =~ s/###event calendar id###/$event{cal_id}[0]/g;
  $return_text =~ s/###event background color###/$event{bgcolor}/g;

  my $event_details = $event{details};
  #replace \n characters with <br> tags
  $event_details =~ s/\n/\n<br>\n/g;
  
  # check the event details, and see if there are any non-htmlified
  # links.  If so, turn them into links.
  $event_details =~ s/[^"](htt.:\/\/.+?),*\.?(\s|\n|<|$)/ <a href=\"$1\">$1<\/a>$2/g;
  
  # convert email addresses to links.
  $event_details = &make_email_link($event_details);

  # make sure all links open up in a new window
  $event_details =~ s/<a/<a target = "blank"/g;
  
  $return_text =~ s/###event details###/$event_details/g;
  
  my $event_icon_text = "";
  if ($event{icon} ne "blank")
  {
    $event_icon_text = "<img style=\"border-width:0px;\" src = \"$icons_url/$event{icon}_50x50.gif\" hspace=2 vspace=1><br>";
  }
  $return_text =~ s/###event icon###/$event_icon_text/g;

  my $unit_number_text = $event{unit_number};
  $unit_number_text =~ s/(\d)/<img src="$graphics_url\/unit_number_patch_$1_40x25.gif" alt="" border="0" vspace=0 hspace=0>/g;
  $return_text =~ s/###unit number icon###/$unit_number_text/g;
  
  my $edit_event_link = "<a target = \"cal_mainwindow\" href=\"$script_url/$name?active_tab=1&add_edit_event=edit&amp;&evt_id=$event{id}$consistent_parameter_string\">$lang{context_menu_edit_event}</a>";
  $edit_event_link = $lang{event_details_edit_disable} unless $writable{events_file};
  $return_text =~ s/###edit event link###/$edit_event_link/g;

  my $delete_event_link = "<a href=\"javascript:return false;\" onMouseUp =\"opener.active_event_id = $event{id}; opener.delete_event()\">$lang{context_menu_delete_event}</a>";
  $delete_event_link = $lang{context_menu_delete_event_disable} unless $writable{events_file};
  $return_text =~ s/###delete event link###/$delete_event_link/g;
  
  my $email_reminder_link = "";
  if ($options{email_mode} != 0)
  {
    $email_reminder_link = "<a href=\"$script_url/$name?email_reminder=1&evt_id=$event{id}\">$lang{email_reminder_link}</a>";
    $email_reminder_link = $lang{event_email_reminder_disable2} unless $writable{email_reminders_datafile};
    $return_text =~ s/###email reminder link###/$email_reminder_link/g;
  }
  else
  {  
    $email_reminder_link = $lang{event_email_reminder_disable2} unless $writable{email_reminders_datafile};
    $return_text =~ s/(<li.+>)?###email reminder link###/$email_reminder_link/g;
  }
  

  my $temp = &export_event_link(\%event);
  $return_text =~ s/###export event link###/$temp/g;

  my $cal_detail_text .= <<p1;
$calendars{$event{cal_ids}[0]}{cal_details}
p1
  
  $return_text =~ s/###event calendar details###/$cal_detail_text/g;

  return $return_text;
} # generate_event_details

sub export_event_link()
{
  my $results = "";
  my ($event_ref) = @_;
  my %event = %{$event_ref};
  
  $results .=<<p1;
<form name="export_event_form" id="export_event_form" target="_blank" action="$script_url/$name" method=GET>
<a href="javascript:document.export_event_form.submit();">$lang{export}</a> $lang{this_event_to}
<input type="hidden" name="export_event" value=1>
<input type="hidden" name="evt_id" value="$event{id}">
<br/>

<select name="export_type" style="font-size:x-small;">
<option value="icalendar">$lang{icalendar_option}
<option value="vcalendar">$lang{vcalendar_option}
<option value="ascii_text">$lang{text_option}
</select>
</form>
p1
} #export_event_link

sub validate_emails()
{
  my ($email_string) = @_;
  
  # support multiple email addresses
  my @to_addresses = split (',', "$email_string");
  
  foreach $to_address (@to_addresses)
  {
    if (!($to_address =~ /^[\w\-\_\.]+\@([\w\-\_]+\.)+[a-zA-Z]{2,}$/))
    {
      return $to_address;
    }  
  }
  return "";
}


sub send_email_reminder()
{
  my ($event_ref, $to_address, $email_text) = @_;
  my %event = %{$event_ref};
  
  if ($options{email_mode} == 0)
    {return $lang{send_email_reminder2};}
    
  $date_string = &nice_date_range_format($event{start}, $event{end}, " - ");
  
  $to_address =~ s/\s//g;
  chomp $to_address;
  
  my $email_valid = &validate_emails($to_address);
  if ($email_valid ne "")
  {
    return "$lang{send_email_reminder1} ($email_valid).";
  }  
  my @to_addresses = split (',', "$to_address");
  
  foreach $temp (@to_addresses)
  {
    my $subject = $lang{send_email_reminder_subject};
    $subject =~ s/###title###/$event{title}/g;
    &send_email($temp, $options{reply_address}, $options{reply_address}, $subject, $email_text);
  }
  return "1";
  #return "$lang{send_email_reminder3} ($options{email_mode}).";
  
} # send_email_reminder


sub send_email()
{
  my ($to, $from, $reply_to, $subject, $body) = @_;
    
  my $content_type = "text/plain";
  
  $body =~ s/\n/\r\n/g;
  
  if ($options{html_email} eq "1")
  {
    $content_type = "text/html";
  
    $body = <<p1;
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>
<head>
<title>$subject</title>
</head>

$body
</body></html>

p1
  }
  
  
  
  if ($options{email_mode} == 1)
  {
    open(SENDMAIL, "| $options{sendmail_location} -t") || ($debug_info .= "Can't open sendmail at $options{sendmail_location}!\n");

    print SENDMAIL <<p1;
Reply-to: $reply_to
From: $from
Subject: $subject
To: $to
Content-type: $content_type\n
$body

p1
    close(SENDMAIL);
  }
  elsif ($options{email_mode} == 2)
  {
    $smtp->mail($reply_to);
    $smtp->to($to);
    $smtp->data();
    $smtp->datasend("To: $to\n");
    $smtp->datasend("From: $from\n");
    $smtp->datasend("Subject: $subject\n");
    $smtp->datasend("Content-type: $content_type\n\n");
    $smtp->datasend("$body\n");
    $smtp->dataend();
    $smtp->reset();
  }
  else
  {
  }
}



sub deep_copy {
  my $this = shift;
  if (not ref $this) {
    $this;
  } elsif (ref $this eq "ARRAY") {
    [map deep_copy($_), @$this];
  } elsif (ref $this eq "HASH") {
    +{map { $_ => deep_copy($this->{$_}) } keys %$this};
  } else { die "what type is $_?" }
}

sub load_file()
{
  my ($file)=@_;
  if (-e $file)
  {
    open (FH, "$file") || (return "unable to open include file $file for reading");
    flock FH,2;
    my @lines=<FH>;
    close FH;
    $text = join "", @lines;
    return $text;
  }
  else
  {
    return "file $file does not exist";
  }
}


sub xml2html
{
	my ($xml) = @_;
	$xml =~ s/</&lt;/gs;
	$xml =~ s/>/&gt;/gs;
	return $xml;
}


# default calendar data structure
#%default_cal;
%default_cal = (id => "", 
                title => "", 
                details => $new_calendar_default_details,
                link => "",
                local_background_calendars => {},
                selectable_calendars => {},
                make_new_calendars_selectable => $options{new_calendars_automatically_selectable},
                list_background_calendars_together => "",
                background_events_display_style => "normal",
                background_events_fade_factor => "",
                background_events_color => "#ffffff",
                default_number_of_months => 1,
                max_number_of_months => 24,
                gmtime_diff => 0,
                date_format => "mm/dd/yy",
                week_start_day => 0,
                preload_event_details => "yes",
                info_window_size => "400x400",
                custom_template => "",
                custom_stylesheet => "",
                password => "",
                update_timestamp => 0);



# If an included file contains only subroutines, perl will complain 
# that it "did not return a true value".  The "return 1;" at the end fixes this.
return 1;
