package kipps.org;

import java.io.File;
import java.util.ListIterator;
import java.util.Vector;

import kipps.basics.ContextWrapper;
import kipps.basics.Ico;
import kipps.basics.KConfirmADialogue;
import kipps.basics.KDateTime;
import kipps.basics.KError;
import kipps.basics.KFile;
import kipps.basics.KFileException;
import kipps.basics.KcsvException;
import kipps.general.KDateRepeater;
import kipps.general.KLG;

/**
 * Created with IntelliJ IDEA.
 * User: Iain
 * Date: 02.05.14
 * Time: 17:44
 * This class contains the platform-independent methods required for managing alarms and reminders for the calendar and action
 * list. It is extended by the PimAlarmManager classes for the individual platforms.
 */

public abstract class PimAlarmManagerCommon
{
  protected ContextWrapper cw ;
  protected KError interror = new KError( "Pim Alarm Manager" ) ;             // For error reporting, identifies the class (also used by derived classes)
  protected String alistSource = null ;                                                  // THe directory under which the alarm list is stored

// Constructor - just save the context and set the source (Directory and file name) of the alarm list. This is either set from system
// parameters or explicitly.

  PimAlarmManagerCommon ( ContextWrapper cwIn , String listSourceIn )
  {
    cw = cwIn ;
    alistSource = listSourceIn == null ? KSD.params.dbdir + File.separator + KSD.params.getSoftwareId() + "_alarminfo.txt" : listSourceIn ;
  }

// Update, add, or remove a single alarm in the alarm list. To be called when a single event or action is edited or deleted. The standard
// update processing is executed. We then check whether the alarm is or was previously set as the system alarm. If so, we reset the system alarm.

  PimAlarm updateOneAlarm ( PimEvent pe )
  {
    PimAlarm procAlarm = updateAlarm ( pe ) ;                                 // This is the alarm we have processed
    PimAlarm newAlarm = getNextValidAlarm() ;                                 // We want this to be the current system alarm now

    if ( newAlarm == null )                                                   // There are no valid alarms after the update
    {
      clearSystemAlarm ( ) ;                                                   // Clear system alarm - don't need to know for which time (just the intent action)
    }
    else
    {
      setSystemAlarm ( newAlarm ) ;                                           // Anything could have happened - just set the new alarm
    }
    return procAlarm ;
  }

  PimAlarm updateOneAlarm ( PimTodo ptd )
  {
    PimAlarm procAlarm = updateAlarm ( ptd ) ;                                // This is the alarm we have processed
    PimAlarm newAlarm = getNextValidAlarm() ;                                 // We want this to be the current system alarm now

    if ( newAlarm == null )                                                   // There are no valid alarms after the update
    {
      clearSystemAlarm ( ) ;                                                   // Clear system alarm - don't need to know for which time (just the intent action)
    }
    else
    {
      setSystemAlarm ( newAlarm ) ;                                           // Anything could have happened - just set the new alarm
    }
    return procAlarm ;
  }

// Add one alarm to the alarm list, without having to know whether it is already present. Return the alarm found, added, or updated.
// An event or action can have only one alarm, so we use the event key to determine if an alarm for the event or action is already set.
// Parameter verursacher is the key of the corresponding event/action record. This operation is called on creation and edit or event
// and action records.
// The alarm repeater is null if the source of the alarm does not repeat. Otherwise it is a copy of the repeater of the source. (Note
// that we cannot modify the repeater to target the alarm times directly - we must use the source repeater and calculate the offset
// each time it is required.
// Note that when an alarm is triggered (platform-dependent), we may not have access to the database. The alarm list is guaranteed to be
// available. Therefore, all information required for alarm display (notification output) and for resetting the trigger MUST be in the
// PimAlarm record. This includes all information to be output in an alarm Meldung and anything required to reset the system alarm and
// the alarm list.
// There are two different signatures for this method, so event alarms and action alarms can be handled differently.

  private PimAlarm updateAlarm ( PimEvent pe )
  {
    KDateTime now = new KDateTime ( ) ;
    KDateTime remT = null ;                                                     // The reminder time, if reminder is valid
    KDateTime evT = null ;                                                       // The corresponding event time for the instance being processed
    PimAlarm alarm = KSD.alarmList.find ( PimAlarm.KEY_URSACHE , pe.pkey ) ;     // Find current alarm for this event, if any

    if ( ! pe.reminder )                                                         // The event does not have / no longer has a reminder
    {
      if ( alarm != null )                                                       // The event had an alarm - remove it
      {
        KSD.alarmList.delete ( alarm ) ;
        alarm = null ;
      }
    }
    else                                                                          // The event has an alarm
    {
      if (pe.repeater == null)                                                    // Simple case, without repetition
      {
        remT = new KDateTime ( pe.getReminderTime ( null ) ) ;
        evT = new KDateTime ( pe.startTime ) ;                                // Simple case, we know the start time of this single instance
//        remT = new KDateTime(pe.startTime);
//        remT.add(KDateTime.MINUTE, -pe.reminderTime);                       // The actual reminder time
        if ( now.compareDateTime(remT) > 0 )                                  // The alarm time is in the past
        {
          remT = null;                                                        // Note no reminder
        }
      }
      else                                                                    // We have a repeating event. Find the next relevant time
      {
        int diffMinutes = pe.getMinutesBefore ( ) ;
        KDateTime posTime = new KDateTime ( now ) ;
        posTime.add ( KDateTime.MINUTE, diffMinutes ) ;                        // Event must be after this time for reminder to be after now
        remT = pe.repeater.nextTimeAfter(posTime);                              // This is the time of the event
        if ( remT != null )                                                     // There is such a time
        {
          evT = new KDateTime ( remT ) ;                                       // The actual event start time for this instance
          remT.add (KDateTime.MINUTE, -diffMinutes ) ;                         // The actual reminder time
        }
      }
    }

// We now have pe as the source element, remT as the actual reminder time, which is null if the time is invalid (i.e. in the past)
// or if the reminder has been edited out the source record. evT is the time of the event instance for which the alarm is required.
// If an alarm record currently exists, it is referenced by alarm.
// Now construct the updated alarm record.

    if ( remT == null )                                        // No valid alarm, delete the alarm record if one is present
    {
      if ( alarm != null )                                     // Alarm record is present, delete it
      {
        KSD.alarmList.delete(alarm);
        alarm = null;
      }
    }
    else                                                       // Here to set a valid alarm time
    {
      alarm = constructAlarm ( pe, alarm, remT , evT ) ;
    }
   return alarm ;                                                               // Pass back the new alarm, or null
  }

// From V 6.0.0 Actions may have two alarms - start action and complete action. This requires that all alarm cration / editing
// be performed for both alarms of an action. The alarm reminder is abolished. The alarm times are taken directly from the
// completion time and (if different) the activation time.

  private PimAlarm updateAlarm ( PimTodo ptd )
  {
    KDateTime now = new KDateTime();
//    KDateTime remT = ptd.reminder ;                                             // The reminder time, if reminder is valid, stored as an absolute time here (not an offset)
    Ico actIco = new Ico();

    PimAlarm ace = KSD.alarmList.startDuplicates(PimAlarm.KEY_URSACHE, actIco, ptd.pkey);
    PimAlarm act = null;
    if (ace != null)
    {
      if (ace.alarmInfo != null && ace.alarmInfo.equals("ACT"))
      {
        act = ace;                                              // This is the record for the completion alarm
        ace = null;                                             // Record for start alarm does not exist (must come first)
      }
      else
      {
        act = KSD.alarmList.next(actIco);                  // Otherwise get the record for the completion alarm (must come second)
      }
    }

    if (ptd.status != PimTodo.PENDING)                   // If completed or cancelled, all alarms concerned with the task should be deleted
    {
      if (act != null)
      {
        KSD.alarmList.delete(act);                              // Delete the alarm record
        ace = null;
      }
      if (ace != null)
      {
        KSD.alarmList.delete(ace);                              // Delete the alarm record
        ace = null;
      }
    }
    else
    {

// First process the alarm for a possible activation of the task - alarm record in ace, if it exists.

      KDateTime firetime = null;                                  // Set alarm time to time required, or null if not required
      if (ptd.duration != 0)
      {
        firetime = new KDateTime(ptd.plannedDate);             // Completion date
        firetime.add(KDateTime.DAY_OF_WEEK, -ptd.duration);      // Get the activation time for the action
        if (now.compareDate(firetime) > 0)                       // If this time is before the present, we don't need an alarm
        {
          firetime = null;
        }
      }

      if (ace != null)                                            // There is a record for a start alarm
      {
        if (firetime == null)                                     // The activation alarm is no longer required
        {
          KSD.alarmList.delete(ace);                              // Delete the alarm record
          ace = null;
        }
        else                                                      // The record exists, must be updated
        {
          ace = constructAlarm(ptd, ace, firetime, "ACE");   // Update the alarm for the new time
        }
      }
      else                                                        // There is no record for a start alarm
      {
        if (firetime != null)                                     // Changes indicate we need this alarm
        {
          ace = constructAlarm(ptd, ace, firetime, "ACE");   // Create it
        }
      }

// Now process the alarm record for the completion of the task, alarm record in act, if it exists

      if (now.compareDateTime(ptd.plannedDate) > 0)            // The alarm time is in the past
      {
        if (act != null)                                             // Delete the alarm, if it exists
        {
          KSD.alarmList.delete(act);
          act = null;
        }
      }
      else
      {
        act = constructAlarm(ptd, act, ptd.plannedDate, "ACT");         // Create or edit the alarm record
      }
    }

    return act ;
  }



 /*

    PimAlarm alarm = KSD.alarmList.find ( PimAlarm.KEY_URSACHE , ptd.pkey ) ;   // Find current alarm for this action, if any

    if ( now.compareDateTime ( remT ) > 0 )                                      // The alarm time is in the past
    {
      remT = null ;                                                              // Cancel the alarm
    }

// We now have ptd as the source element, remT as the actual reminder time, which is null if the time is invalid (i.e. in the past)
// or if the reminder has been edited out of the source record.
// If an alarm record currently exists, it is referenced by alarm.
// Now construct the updated alarm record.

    if ( remT == null )                                                      // No valid alarm, delete the record if one is present
    {
      if ( alarm != null )                                                   // Alarm record is present, delete it
      {
        KSD.alarmList.delete(alarm);
        alarm = null;
      }
    }
    else
    {
      alarm = constructAlarm ( ptd, alarm, remT ) ;
    }
    return alarm ;                                                               // Pass back the new alarm, or null
  }
*/
// Construct one alarm record.
// We have different entry points for event alarms and action alarms. The method creates an alarm record with all the information required
// to show a notification when the alarm fires. Note that at firing time the full database may not be available in the case of issuing
// notifications on Android systems.

// This is the variant for calendar alarms. The messages to be output depend on how far in the future the event is planned, and whether or
// not it is an all day event. The parameter startTime gives the start time of the instance for which the alarm should be constructed. For
// series, this is not the same as the start time in the source record.

  private PimAlarm constructAlarm ( PimEvent source , PimAlarm curalarm , KDateTime firingTime , KDateTime startTime )
  {
    if ( curalarm == null )
    {
      curalarm = new PimAlarm ( source.pkey , firingTime ) ;                            // Alarm for this verursacher does not exist, create
      KSD.alarmList.add ( curalarm ) ;
    }
    else
    {
      curalarm.alarmTime = firingTime ;                                                 // Update existing value of the time of the alarm
      curalarm.noteUpdated();
      KSD.alarmList.setKeyvalueChanged ( ) ;                                            // The alarm time is a key
    }

 //   curalarm.ablauf = source.endTime + KSD.params.eventAlarmAblauf ;                    // Note latest delete time for notification

    curalarm.tone = true ;                                                              // Always have an audible signal, for now

    curalarm.alarmType = "CAL" ;                                                        // This is a calendar alarm

    int diffDays = startTime.differenceDays ( curalarm.alarmTime ) ;          // Text depends on how far in advance
    int diffMins = startTime.differenceMinutes ( curalarm.alarmTime ) ;

    if ( source.noTime )                                                         // Special headings for all day events
    {
      curalarm.alarmHeading2 = KL.gtd(  "All day event on ",  "Ganztagsereignis am " ) + source.startTime.dateStringShort() ;
      if (diffDays == 0)
      {
        curalarm.alarmHeading = KL.gtd ( "All day event today", "Ganztagsereignis heute" ) ;
      }
      else
      {
        if (diffDays == 1)
        {
          curalarm.alarmHeading = KL.gtd ( "All day event tomorrow", "Ganztagsereignis morgen" ) ;
        }
        else
        {
          curalarm.alarmHeading = curalarm.alarmHeading2 ;
        }
      }
    }
    else                                                                      // Headings for timed events
    {
      curalarm.alarmHeading2 = KL.gtd("Appointment on ", "Termin am ") + source.startTime.dateStringShort() + KL.gtd(" at ", " um ") + source.startTime.timeString();
      if (diffMins == 0)
      {
        curalarm.alarmHeading = KL.gtd("Appointment NOW (", "Termin JETZT (") + source.startTime.timeString() + ")";
      }
      else
      {
        if ( diffDays == 0 )
        {
          curalarm.alarmHeading = KL.gtd("Appointment today at ", "Termin heute um ") + source.startTime.timeString();
        }
        else
        {
          if (diffDays == 1)
          {
            curalarm.alarmHeading = KL.gtd("Appointment tomorrow at ", "Termin morgen um ") + source.startTime.timeString();
          }
          else
          {
            curalarm.alarmHeading = curalarm.alarmHeading2 ;
          }
        }
      }
    }

    curalarm.alarmInfo = source.formatHeading();
    curalarm.alarmRepeat = source.repeater == null ? null : new KDateRepeater ( source.repeater ) ;    // May be null, needed to create next alarm on firing
    curalarm.offset = source.getMinutesBefore() ;                                                      // Only relevant if a repeater is present, otherwise 0

    return curalarm ;
  }

// This is the variant for action alarms. We have two sorts of alarm, so the type info is passed in kennung (ACE or ACT).

  private PimAlarm constructAlarm ( PimTodo source , PimAlarm curalarm , KDateTime firingTime , String kennung )
  {
    if ( curalarm == null )
    {
      curalarm = new PimAlarm ( source.pkey , firingTime ) ;                            // Alarm for this verursacher does not exist, create
      KSD.alarmList.add ( curalarm ) ;
    }
    else
    {
      curalarm.alarmTime = firingTime ;                                                 // Update existing value of the time of the alarm
      curalarm.noteUpdated();
      KSD.alarmList.setKeyvalueChanged ( ) ;                                            // The alarm time is a key
    }
    curalarm.tone = true ;                                                              // Always have an audible signal, for now

    curalarm.alarmType = kennung ;                                                      // This is an action alarm, start or finish

    if ( kennung.equals ( "ACE") )
    {
      curalarm.alarmHeading = KL.gtd ( "Task activated on ", "Aufgabe aktiviert am ") + firingTime.dateString(); // source.activationDate.dateStringShort() ;
    }
    else
    {
      curalarm.alarmHeading = KL.gtd ( "Task due on ", "Aufgabe fällig am ") + firingTime.dateString();
    }
    curalarm.alarmHeading2 = curalarm.alarmHeading ;
    curalarm.alarmInfo = source.formatShortHeading () ;
    curalarm.alarmRepeat = null ;                                          // Actions never repeat automatically
    curalarm.offset = 0 ;                                                  // Reminder offset never relevant

    return curalarm ;
  }

// Examine the alarm list and return the alarm that is the first in the future. This should be - but may not be - the
// first element in the list. If there is no valid alarm, the method returns null. Used internally to get the next alarm due after
// an alarm has been added or deleted and in the alarm handler (CAlarm.java) to get the next alarm due when an alarm is sounded.
// Note that from IK-Org V6 onwards, alarms with a date IN THE PAST may still be in the list. This will not affect this method
// because we start searching for alarms at the current date loking towards the future.

  PimAlarm getNextValidAlarm ( )
  {
    KDateTime now = new KDateTime( ) ;
    PimAlarm nextAlarm ;

    Ico iter = new Ico( ) ;
    nextAlarm = KSD.alarmList.start ( PimAlarm.KEY_ATIME , iter , now  ) ;         // Read alarms in order of time to be sounded, starting at current time
    return nextAlarm ;                                                             // Return first alarm at or after current time
  }

// Recreate the alarm list from the data in the event and action lists. This is used on startup and after synchronisation (the alarm list
// cannot be synchronised), and may be used after editing elements that may give rise to alarms.

  void recreateAlarmList ( )
  {
    KDateTime now = new KDateTime ( ) ;                                            // No reminders before this time
    KSD.alarmList = new PimAlarmList ( 10 ) ;                              // If an alarm list exists, it is overwritten

    Ico evIco = new Ico ( ) ;
    PimEvent pe = KSD.eventList.start ( PimEvent.PKEY , evIco ) ;                   // Read the entire list
    while ( pe != null )
    {
      updateAlarm ( pe ) ;
      pe = KSD.eventList.next(evIco);
    }

    PimTodo ptd =  KSD.todoList.start ( PimTodo.PKEY , evIco ) ;

    while ( ptd != null )                                                            // Only have the current element, no need to evaluate repeaters
    {
      updateAlarm ( ptd ) ;
      ptd = KSD.todoList.next(evIco);
    }

    PimAlarm alarm = getNextValidAlarm ( ) ;                                   // Now set the system alarm (Android only)

    if ( alarm == null )
    {
      clearSystemAlarm ( ) ;                                                   // Clear system alarm - don't need to know for which time (just the intent action)
    }
    else
    {
      setSystemAlarm ( alarm ) ;                                                // Anything could have happened - just set the new alarm
    }

    return ;
  }

// The specified alarm has been sent to the system as a notification, or is not going to be sent because it is already in the past for some reason,
// e.g. synchronisation. If a single alarm, it should be removed from the alarm list. If a repeating alarm with valid repeats, the next valid repeat
// is set as the alarm time.

  void removeAfterFiring ( PimAlarm alarm )
  {
    if ( alarm.alarmRepeat == null )                // Not a repeating alarm, just delete
    {
      KSD.alarmList.delete ( alarm ) ;
    }
    else
    {
      KDateTime minATime = new KDateTime ( ) ;
      minATime.add ( KDateTime.MINUTE , alarm.offset ) ;                  // The next valid repeat must be after this time
      KDateTime nextT = alarm.alarmRepeat.nextTimeAfter ( minATime ) ;    // The time of the first valid occurrence
      if ( nextT == null )                                                // No further occurrences to process
      {
        KSD.alarmList.delete ( alarm ) ;                                  // Delete it
      }
      else
      {
        nextT.add ( KDateTime.MINUTE, -alarm.offset);                     // The time of the alarm for this occurrence
        alarm.alarmTime = nextT ;                                         // Set this time
        alarm.noteUpdated();
        KSD.alarmList.setKeyvalueChanged ( ) ;                            // Time is a key
      }
    }
    return ;
  }

// Write a copy of the alarm file, always in unencripted form. We store the alarm file separately from the database so
// it can be easily loaded even if the database is not loaded, and so that it will never be synchronised.

  String saveAlarmList (  )
  {
    String lastError = null ;
//    String savealarm = alistSource + File.separator + KSD.params.getSoftwareId() + "_alarminfo.txt" ;

    KFile alammFile =  new KFile ( alistSource ) ;
    alammFile.setInterchangeEncoding ( ) ;                 // Standard plattformübergreifende Encoding
    try
    {
      alammFile.open_write();
      KSD.alarmList.save ( alammFile ) ;
      alammFile.close();
    }
    catch ( KFileException ke )
    {
      lastError = KLG.gt ( KLG.ip_m10 ) + "\n" + ke.getDetails() ;

      new KConfirmADialogue( KL.gt ( KL.cont ) , null , KL.gt ( KL.al_h1 ) , lastError , null , cw ) ;
    }

    return lastError ;
  }

// Restore a copy of the alarm list, which is held outside the database. If the restore fails, reurn the error message
// string, otherwise (if successsful) return null.

  String restoreAlarmList (  )
  {
    String lastError = null ;

//    String savealarm = alistSource + File.separator + KSD.params.getSoftwareId() + "_alarminfo.txt" ;
    KFile alammFile =  new KFile ( alistSource ) ;
    alammFile.setInterchangeEncoding ( ) ;                 // Standard plattformübergreifende Encoding
    if ( ! alammFile.open_read ( ) )
    {
      lastError = KLG.gtd ( "Alarm list not available" , "Alarmliste nicht gefunden" ) ;
    }
    else
    {
      try
      {
        KSD.alarmList = new PimAlarmList(20);
        try
        {
          KSD.alarmList.restore(alammFile);
        }
        catch ( KcsvException ke )
        {
          alammFile.close() ;                              // Close file if possible - should never happen
          lastError = KLG.gt ( KLG.ip_m10 ) + "\n" + ke.getDetails() ;
        }
        alammFile.close();
      }
      catch ( KFileException kfe )
      {
        lastError = KLG.gt ( KLG.ip_m10 ) + "\n" + kfe.getDetails() ;
      }
    }
    return lastError ;                                       // Return null if list could be restored
  }

  abstract void setSystemAlarm ( PimAlarm alarm ) ;
  abstract void clearSystemAlarm ( ) ;
}

// *************** End of class PimAlarmManagerCommon ***************
