/*
  This file is part of the kcalcore library.

  Copyright (c) 1998 Preston Brown <pbrown@kde.org>
  Copyright (c) 2001 Cornelius Schumacher <schumacher@kde.org>
  Copyright (c) 2003 David Jarvie <software@astrojar.org.uk>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Library General Public License for more details.

  You should have received a copy of the GNU Library General Public License
  along with this library; see the file COPYING.LIB.  If not, write to
  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  Boston, MA 02110-1301, USA.
*/
/**
  @file
  This file is part of the API for handling calendar data and
  defines the Alarm class.

  @brief
  Represents an alarm notification.

  @author Cornelius Schumacher \<schumacher@kde.org\>
*/
#include "alarm.h"
#include "duration.h"
#include "incidence.h"

using namespace KCalCore;

/**
  Private class that helps to provide binary compatibility between releases.
  @internal
*/
//@cond PRIVATE
class KCalCore::Alarm::Private
{
  public:
    Private()
      : mParent( 0 ),
        mType( Alarm::Invalid ),
        mAlarmSnoozeTime( 5 ),
        mAlarmRepeatCount( 0 ),
        mEndOffset( false ),
        mHasTime( false ),
        mAlarmEnabled( false ),
        mHasLocationRadius ( false ),
        mLocationRadius ( 0 )
    {}
    Private( const Private &other )
      : mParent( other.mParent ),
        mType( other.mType ),
        mDescription( other.mDescription ),
        mFile( other.mFile ),
        mMailSubject( other.mMailSubject ),
        mMailAttachFiles( other.mMailAttachFiles ),
        mMailAddresses( other.mMailAddresses ),
        mAlarmTime( other.mAlarmTime ),
        mAlarmSnoozeTime( other.mAlarmSnoozeTime ),
        mAlarmRepeatCount( other.mAlarmRepeatCount ),
        mOffset( other.mOffset ),
        mEndOffset( other.mEndOffset ),
        mHasTime( other.mHasTime ),
        mAlarmEnabled( other.mAlarmEnabled ),
        mHasLocationRadius( other.mHasLocationRadius ),
        mLocationRadius( other.mLocationRadius )
    {}

    Incidence *mParent;  // the incidence which this alarm belongs to

    Type mType;          // type of alarm
    QString mDescription;// text to display/email body/procedure arguments
    QString mFile;       // program to run/optional audio file to play
    QString mMailSubject;// subject of email
    QStringList mMailAttachFiles; // filenames to attach to email
    Person::List mMailAddresses;  // who to mail for reminder

    KDateTime mAlarmTime;// time at which to trigger the alarm
    Duration mAlarmSnoozeTime; // how long after alarm to snooze before
                               // triggering again
    int mAlarmRepeatCount;// number of times for alarm to repeat
                          // after the initial time

    Duration mOffset;    // time relative to incidence DTSTART
                         // to trigger the alarm
    bool mEndOffset;     // if true, mOffset relates to DTEND, not DTSTART
    bool mHasTime;       // use mAlarmTime, not mOffset
    bool mAlarmEnabled;

    bool mHasLocationRadius;
    int mLocationRadius; // location radius for the alarm
};
//@endcond

Alarm::Alarm( Incidence *parent ) : d( new KCalCore::Alarm::Private )
{
  d->mParent = parent;
}

Alarm::Alarm( const Alarm &other ) :
  CustomProperties( other ), d( new KCalCore::Alarm::Private( *other.d ) )
{
}

Alarm::~Alarm()
{
  delete d;
}

Alarm &Alarm::operator=( const Alarm &a )
{
  if ( &a != this ) {
    d->mParent = a.d->mParent;
    d->mType = a.d->mType;
    d->mDescription = a.d->mDescription;
    d->mFile = a.d->mFile;
    d->mMailAttachFiles = a.d->mMailAttachFiles;
    d->mMailAddresses = a.d->mMailAddresses;
    d->mMailSubject = a.d->mMailSubject;
    d->mAlarmSnoozeTime = a.d->mAlarmSnoozeTime;
    d->mAlarmRepeatCount = a.d->mAlarmRepeatCount;
    d->mAlarmTime = a.d->mAlarmTime;
    d->mOffset = a.d->mOffset;
    d->mEndOffset = a.d->mEndOffset;
    d->mHasTime = a.d->mHasTime;
    d->mAlarmEnabled = a.d->mAlarmEnabled;
  }

  return *this;
}

bool Alarm::operator==( const Alarm &rhs ) const
{
  if ( d->mType != rhs.d->mType ||
       d->mAlarmSnoozeTime != rhs.d->mAlarmSnoozeTime ||
       d->mAlarmRepeatCount != rhs.d->mAlarmRepeatCount ||
       d->mAlarmEnabled != rhs.d->mAlarmEnabled ||
       d->mHasTime != rhs.d->mHasTime ||
       d->mHasLocationRadius != rhs.d->mHasLocationRadius ||
       d->mLocationRadius != rhs.d->mLocationRadius ) {
    return false;
  }

  if ( d->mHasTime ) {
    if ( d->mAlarmTime != rhs.d->mAlarmTime ) {
      return false;
    }
  } else {
    if ( d->mOffset != rhs.d->mOffset || d->mEndOffset != rhs.d->mEndOffset ) {
      return false;
    }
  }

  switch ( d->mType ) {
    case Display:
      return d->mDescription == rhs.d->mDescription;

    case Email:
      return d->mDescription == rhs.d->mDescription &&
             d->mMailAttachFiles == rhs.d->mMailAttachFiles &&
             d->mMailAddresses == rhs.d->mMailAddresses &&
             d->mMailSubject == rhs.d->mMailSubject;

    case Procedure:
      return d->mFile == rhs.d->mFile &&
             d->mDescription == rhs.d->mDescription;

    case Audio:
      return d->mFile == rhs.d->mFile;

    case Invalid:
      break;
  }
  return false;
}

bool Alarm::operator!=( const Alarm &a ) const
{
  return !operator==( a );
}

void Alarm::setType( Alarm::Type type )
{
  if ( type == d->mType ) {
    return;
  }

  if ( d->mParent ) {
    d->mParent->update();
  }
  switch ( type ) {
    case Display:
      d->mDescription = "";
      break;
    case Procedure:
      d->mFile = d->mDescription = "";
      break;
    case Audio:
      d->mFile = "";
      break;
    case Email:
      d->mMailSubject = d->mDescription = "";
      d->mMailAddresses.clear();
      d->mMailAttachFiles.clear();
      break;
    case Invalid:
      break;
    default:
      if ( d->mParent ) {
        d->mParent->updated(); // not really
      }
      return;
  }
  d->mType = type;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

Alarm::Type Alarm::type() const
{
  return d->mType;
}

void Alarm::setAudioAlarm( const QString &audioFile )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mType = Audio;
  d->mFile = audioFile;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

void Alarm::setAudioFile( const QString &audioFile )
{
  if ( d->mType == Audio ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mFile = audioFile;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

QString Alarm::audioFile() const
{
  return ( d->mType == Audio ) ? d->mFile : QString();
}

void Alarm::setProcedureAlarm( const QString &programFile,
                               const QString &arguments )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mType = Procedure;
  d->mFile = programFile;
  d->mDescription = arguments;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

void Alarm::setProgramFile( const QString &programFile )
{
  if ( d->mType == Procedure ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mFile = programFile;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

QString Alarm::programFile() const
{
  return ( d->mType == Procedure ) ? d->mFile : QString();
}

void Alarm::setProgramArguments( const QString &arguments )
{
  if ( d->mType == Procedure ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mDescription = arguments;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

QString Alarm::programArguments() const
{
  return ( d->mType == Procedure ) ? d->mDescription : QString();
}

void Alarm::setEmailAlarm( const QString &subject, const QString &text,
                           const Person::List &addressees,
                           const QStringList &attachments )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mType = Email;
  d->mMailSubject = subject;
  d->mDescription = text;
  d->mMailAddresses = addressees;
  d->mMailAttachFiles = attachments;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

void Alarm::setMailAddress( const Person::Ptr &mailAddress )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mMailAddresses.clear();
    d->mMailAddresses.append( mailAddress );
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

void Alarm::setMailAddresses( const Person::List &mailAddresses )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mMailAddresses += mailAddresses;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

void Alarm::addMailAddress( const Person::Ptr &mailAddress )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mMailAddresses.append( mailAddress );
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

Person::List Alarm::mailAddresses() const
{
  return ( d->mType == Email ) ? d->mMailAddresses : Person::List();
}

void Alarm::setMailSubject( const QString &mailAlarmSubject )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mMailSubject = mailAlarmSubject;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

QString Alarm::mailSubject() const
{
  return ( d->mType == Email ) ? d->mMailSubject : QString();
}

void Alarm::setMailAttachment( const QString &mailAttachFile )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mMailAttachFiles.clear();
    d->mMailAttachFiles += mailAttachFile;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

void Alarm::setMailAttachments( const QStringList &mailAttachFiles )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mMailAttachFiles = mailAttachFiles;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

void Alarm::addMailAttachment( const QString &mailAttachFile )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mMailAttachFiles += mailAttachFile;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

QStringList Alarm::mailAttachments() const
{
  return ( d->mType == Email ) ? d->mMailAttachFiles : QStringList();
}

void Alarm::setMailText( const QString &text )
{
  if ( d->mType == Email ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mDescription = text;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

QString Alarm::mailText() const
{
  return ( d->mType == Email ) ? d->mDescription : QString();
}

void Alarm::setDisplayAlarm( const QString &text )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mType = Display;
  if ( !text.isNull() ) {
    d->mDescription = text;
  }
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

void Alarm::setText( const QString &text )
{
  if ( d->mType == Display ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mDescription = text;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

QString Alarm::text() const
{
  return ( d->mType == Display ) ? d->mDescription : QString();
}

void Alarm::setTime( const KDateTime &alarmTime )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mAlarmTime = alarmTime;
  d->mHasTime = true;

  if ( d->mParent ) {
    d->mParent->updated();
  }
}

KDateTime Alarm::time() const
{
  if ( hasTime() ) {
    return d->mAlarmTime;
  } else if ( d->mParent ) {
    if ( d->mEndOffset ) {
      KDateTime dt = d->mParent->dateTime( Incidence::RoleAlarmEndOffset );
      return d->mOffset.end( dt );
    } else {
      KDateTime dt = d->mParent->dateTime( Incidence::RoleAlarmStartOffset );
      return d->mOffset.end( dt );
    }
  } else {
    return KDateTime();
  }
}

KDateTime Alarm::nextTime( const KDateTime &preTime, bool ignoreRepetitions ) const
{
  if ( d->mParent && d->mParent->recurs() ) {
    KDateTime dtEnd = d->mParent->dateTime( Incidence::RoleAlarmEndOffset );

    KDateTime dtStart = d->mParent->dtStart();
    // Find the incidence's earliest alarm
    // Alarm time is defined by an offset from the event start or end time.
    KDateTime alarmStart = d->mOffset.end( d->mEndOffset ? dtEnd : dtStart );
    // Find the offset from the event start time, which is also used as the
    // offset from the recurrence time.
    Duration alarmOffset( dtStart, alarmStart );
    /*
    kDebug() << "dtStart       " << dtStart;
    kDebug() << "dtEnd         " << dtEnd;
    kDebug() << "alarmStart    " << alarmStart;
    kDebug() << "alarmOffset   " << alarmOffset.value();
    kDebug() << "preTime       " << preTime;
    */
    if ( alarmStart > preTime ) {
      // No need to go further.
      return alarmStart;
    }
    if ( d->mAlarmRepeatCount && !ignoreRepetitions ) {
      // The alarm has repetitions, so check whether repetitions of previous
      // recurrences happen after given time.
      KDateTime prevRecurrence = d->mParent->recurrence()->getPreviousDateTime( preTime );
      if ( prevRecurrence.isValid() ) {
        KDateTime prevLastRepeat = alarmOffset.end( duration().end( prevRecurrence ) );
        // kDebug() << "prevRecurrence" << prevRecurrence;
        // kDebug() << "prevLastRepeat" << prevLastRepeat;
        if ( prevLastRepeat > preTime ) {
	  // Yes they did, return alarm offset to previous recurrence.
          KDateTime prevAlarm = alarmOffset.end( prevRecurrence );
          // kDebug() << "prevAlarm     " << prevAlarm;
          return prevAlarm;
        }
      }
    }
    // Check the next recurrence now.
    KDateTime nextRecurrence = d->mParent->recurrence()->getNextDateTime( preTime );
    if ( nextRecurrence.isValid() ) {
      KDateTime nextAlarm = alarmOffset.end( nextRecurrence );
      /*
      kDebug() << "nextRecurrence" << nextRecurrence;
      kDebug() << "nextAlarm     " << nextAlarm;
      */
      if ( nextAlarm > preTime ) {
	// It's first alarm takes place after given time.
        return nextAlarm;
      }
    }
  } else {
    // Not recurring.
    KDateTime alarmTime = time();
    if ( alarmTime > preTime ) {
      return alarmTime;
    }
  }
  return KDateTime();
}

bool Alarm::hasTime() const
{
  return d->mHasTime;
}

void Alarm::shiftTimes( const KDateTime::Spec &oldSpec,
                        const KDateTime::Spec &newSpec )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mAlarmTime = d->mAlarmTime.toTimeSpec( oldSpec );
  d->mAlarmTime.setTimeSpec( newSpec );
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

void Alarm::setSnoozeTime( const Duration &alarmSnoozeTime )
{
  if ( alarmSnoozeTime.value() > 0 ) {
    if ( d->mParent ) {
      d->mParent->update();
    }
    d->mAlarmSnoozeTime = alarmSnoozeTime;
    if ( d->mParent ) {
      d->mParent->updated();
    }
  }
}

Duration Alarm::snoozeTime() const
{
  return d->mAlarmSnoozeTime;
}

void Alarm::setRepeatCount( int alarmRepeatCount )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mAlarmRepeatCount = alarmRepeatCount;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

int Alarm::repeatCount() const
{
  return d->mAlarmRepeatCount;
}

Duration Alarm::duration() const
{
  return Duration( d->mAlarmSnoozeTime.value() * d->mAlarmRepeatCount,
                   d->mAlarmSnoozeTime.type() );
}

KDateTime Alarm::nextRepetition( const KDateTime &preTime ) const
{
  KDateTime at = nextTime( preTime );
  if ( at > preTime ) {
    return at;
  }
  if ( !d->mAlarmRepeatCount ) {
    // there isn't an occurrence after the specified time
    return KDateTime();
  }
  qint64 repetition;
  int interval = d->mAlarmSnoozeTime.value();
  bool daily = d->mAlarmSnoozeTime.isDaily();
  if ( daily ) {
    int daysTo = at.daysTo( preTime );
    if ( !preTime.isDateOnly() && preTime.time() <= at.time() ) {
      --daysTo;
    }
    repetition = daysTo / interval + 1;
  } else {
    repetition = at.secsTo_long( preTime ) / interval + 1;
  }
  if ( repetition > d->mAlarmRepeatCount ) {
    // all repetitions have finished before the specified time
    return KDateTime();
  }
  return daily ? at.addDays( int( repetition * interval ) )
               : at.addSecs( repetition * interval );
}

KDateTime Alarm::previousRepetition( const KDateTime &afterTime ) const
{
  KDateTime at = time();
  if ( at >= afterTime ) {
    // alarm's first/only time is at/after the specified time
    return KDateTime();
  }
  if ( !d->mAlarmRepeatCount ) {
    return at;
  }
  qint64 repetition;
  int interval = d->mAlarmSnoozeTime.value();
  bool daily = d->mAlarmSnoozeTime.isDaily();
  if ( daily ) {
    int daysTo = at.daysTo( afterTime );
    if ( afterTime.isDateOnly() || afterTime.time() <= at.time() ) {
      --daysTo;
    }
    repetition = daysTo / interval;
  } else {
    repetition = ( at.secsTo_long( afterTime ) - 1 ) / interval;
  }
  if ( repetition > d->mAlarmRepeatCount ) {
    repetition = d->mAlarmRepeatCount;
  }
  return daily ? at.addDays( int( repetition * interval ) )
               : at.addSecs( repetition * interval );
}

KDateTime Alarm::endTime() const
{
  if ( !d->mAlarmRepeatCount ) {
    return time();
  }
  if ( d->mAlarmSnoozeTime.isDaily() ) {
    return time().addDays( d->mAlarmRepeatCount * d->mAlarmSnoozeTime.asDays() );
  } else {
    return time().addSecs( d->mAlarmRepeatCount * d->mAlarmSnoozeTime.asSeconds() );
  }
}

void Alarm::toggleAlarm()
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mAlarmEnabled = !d->mAlarmEnabled;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

void Alarm::setEnabled( bool enable )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mAlarmEnabled = enable;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

bool Alarm::enabled() const
{
  return d->mAlarmEnabled;
}

void Alarm::setStartOffset( const Duration &offset )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mOffset = offset;
  d->mEndOffset = false;
  d->mHasTime = false;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

Duration Alarm::startOffset() const
{
  return ( d->mHasTime || d->mEndOffset ) ? Duration( 0 ) : d->mOffset;
}

bool Alarm::hasStartOffset() const
{
  return !d->mHasTime && !d->mEndOffset;
}

bool Alarm::hasEndOffset() const
{
  return !d->mHasTime && d->mEndOffset;
}

void Alarm::setEndOffset( const Duration &offset )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mOffset = offset;
  d->mEndOffset = true;
  d->mHasTime = false;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

Duration Alarm::endOffset() const
{
  return ( d->mHasTime || !d->mEndOffset ) ? Duration( 0 ) : d->mOffset;
}

void Alarm::setParent( Incidence *parent )
{
  d->mParent = parent;
}

QString Alarm::parentUid() const
{
  return d->mParent ? d->mParent->uid() : QString();
}

void Alarm::customPropertyUpdated()
{
  if ( d->mParent ) {
    d->mParent->update();
    d->mParent->updated();
  }
}

void Alarm::setHasLocationRadius( bool hasLocationRadius )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mHasLocationRadius = hasLocationRadius;
  if ( hasLocationRadius ) {
    setNonKDECustomProperty( "X-LOCATION-RADIUS", QString::number( d->mLocationRadius ) );
  } else {
    removeNonKDECustomProperty( "X-LOCATION-RADIUS" );
  }
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

bool Alarm::hasLocationRadius() const
{
  return d->mHasLocationRadius;
}

void Alarm::setLocationRadius( int locationRadius )
{
  if ( d->mParent ) {
    d->mParent->update();
  }
  d->mLocationRadius = locationRadius;
  if ( d->mParent ) {
    d->mParent->updated();
  }
}

int Alarm::locationRadius() const
{
  return d->mLocationRadius;
}

void Alarm::virtual_hook( int id, void *data )
{
  Q_UNUSED( id );
  Q_UNUSED( data );
  Q_ASSERT( false );
}
