/*
 *   This file only:
 *     Copyright (C) 2003, 2004  Mark Bucciarelli <mark@hubcapconsulting.com>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License along
 *   with this program; if not, write to the
 *      Free Software Foundation, Inc.
 *      51 Franklin Street, Fifth Floor
 *      Boston, MA  02110-1301  USA.
 *
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <cassert>

#include <tqfile.h>
#include <tqsize.h>
#include <tqdict.h>
#include <tqdatetime.h>
#include <tqstring.h>
#include <tqstringlist.h>

#include "incidence.h"
#include "kapplication.h"       // kapp
#include <kdebug.h>
#include <kemailsettings.h>
#include <klocale.h>            // i18n
#include <kmessagebox.h>
#include <kprogress.h>
#include <ktempfile.h>
#include <resourcecalendar.h>
#include <resourcelocal.h>
#include <resourceremote.h>
#include <kpimprefs.h>
#include <taskview.h>
#include <timekard.h>
#include <karmutility.h>
#include <kio/netaccess.h>
#include <kurl.h>
#include <vector>

//#include <calendarlocal.h>
//#include <journal.h>
//#include <event.h>
//#include <todo.h>

#include "karmstorage.h"
#include "preferences.h"
#include "task.h"
#include "reportcriteria.h"

using namespace std;

KarmStorage *KarmStorage::_instance = 0;
static long linenr;  // how many lines written by printTaskHistory so far


KarmStorage *KarmStorage::instance()
{
  if (_instance == 0) _instance = new KarmStorage();
  return _instance;
}

KarmStorage::KarmStorage()
{
  _calendar = 0;
}

TQString KarmStorage::load (TaskView* view, const Preferences* preferences, TQString fileName )
// loads data from filename into view. If no filename is given, filename from preferences is used.
// filename might be of use if this program is run as embedded konqueror plugin.
{
  // When I tried raising an exception from this method, the compiler
  // complained that exceptions are not allowed.  Not sure how apps
  // typically handle error conditions in KDE, but I'll return the error
  // as a string (empty is no error).  -- Mark, Aug 8, 2003

  // Use KDE_CXXFLAGS=$(USE_EXCEPTIONS) in Makefile.am if you want to use
  // exceptions (David Faure)

  TQString err;
  KEMailSettings settings;
  if ( fileName.isEmpty() ) fileName = preferences->iCalFile();

  // If same file, don't reload
  if ( fileName == _icalfile ) return err;


  // If file doesn't exist, create a blank one to avoid ResourceLocal load
  // error.  We make it user and group read/write, others read.  This is
  // masked by the users umask.  (See man creat)
  if ( ! remoteResource( _icalfile ) )
  {
    int handle;
    handle = open (
        TQFile::encodeName( fileName ),
        O_CREAT|O_EXCL|O_WRONLY,
        S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH
        );
    if (handle != -1) close(handle);
  }

  if ( _calendar)
    closeStorage(view);

  // Create local file resource and add to resources
  _icalfile = fileName;

  KCal::ResourceCached *resource;
  if ( remoteResource( _icalfile ) )
  {
    KURL url( _icalfile );
    resource = new KCal::ResourceRemote( url, url ); // same url for upload and download
  }
  else
  {
    resource = new KCal::ResourceLocal( _icalfile );
  }
  _calendar = resource;

  TQObject::connect (_calendar, TQT_SIGNAL(resourceChanged(ResourceCalendar *)),
  	            view, TQT_SLOT(iCalFileModified(ResourceCalendar *)));
  _calendar->setTimeZoneId( KPimPrefs::timezone() );
  _calendar->setResourceName( TQString::fromLatin1("KArm") );
  _calendar->open();
  _calendar->load();

  // Claim ownership of iCalendar file if no one else has.
  KCal::Person owner = resource->getOwner();
  if ( owner.isEmpty() )
  {
    resource->setOwner( KCal::Person(
          settings.getSetting( KEMailSettings::RealName ),
          settings.getSetting( KEMailSettings::EmailAddress ) ) );
  }

  // Build task view from iCal data
  if (!err)
  {
    KCal::Todo::List todoList;
    KCal::Todo::List::ConstIterator todo;
    TQDict< Task > map;

    // Build dictionary to look up Task object from Todo uid.  Each task is a
    // TQListViewItem, and is initially added with the view as the parent.
    todoList = _calendar->rawTodos();
    kdDebug(5970) << "KarmStorage::load "
      << "rawTodo count (includes completed todos) ="
      << todoList.count() << endl;
    for( todo = todoList.begin(); todo != todoList.end(); ++todo )
    {
      // Initially, if a task was complete, it was removed from the view.
      // However, this increased the complexity of reporting on task history.
      //
      // For example, if a task is complete yet has time logged to it during
      // the date range specified on the history report, we have to figure out
      // how that task fits into the task hierarchy.  Currently, this
      // structure is held in memory by the structure in the list view.
      //
      // I considered creating a second tree that held the full structure of
      // all complete and incomplete tasks.  But this seemed to much of a
      // change with an impending beta release and a full todo list.
      //
      // Hence this "solution".  Include completed tasks, but mark them as
      // inactive in the view.
      //
      //if ((*todo)->isCompleted()) continue;

      Task* task = new Task(*todo, view);
      map.insert( (*todo)->uid(), task );
      view->setRootIsDecorated(true);
      task->setPixmapProgress();
    }

    // Load each task under it's parent task.
    for( todo = todoList.begin(); todo != todoList.end(); ++todo )
    {
      Task* task = map.find( (*todo)->uid() );

      // No relatedTo incident just means this is a top-level task.
      if ( (*todo)->relatedTo() )
      {
        Task* newParent = map.find( (*todo)->relatedToUid() );

        // Complete the loading but return a message
        if ( !newParent )
          err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
            .arg(task->name())
            .arg((*todo)->relatedToUid());

        if (!err) task->move( newParent);
      }
    }

    kdDebug(5970) << "KarmStorage::load - loaded " << view->count()
      << " tasks from " << _icalfile << endl;
  }

  return err;
}

TQString KarmStorage::icalfile()
{
  kdDebug(5970) << "Entering KarmStorage::icalfile" << endl;
  return _icalfile;
}

TQString KarmStorage::buildTaskView(KCal::ResourceCalendar *rc, TaskView *view)
// makes *view contain the tasks out of *rc.
{
  TQString err;
  KCal::Todo::List todoList;
  KCal::Todo::List::ConstIterator todo;
  TQDict< Task > map;
  vector<TQString> runningTasks;
  vector<TQDateTime> startTimes;

  // remember tasks that are running and their start times
  for ( int i=0; i<view->count(); i++)
  {
    if ( view->item_at_index(i)->isRunning() )
    {
      runningTasks.push_back( view->item_at_index(i)->uid() );
      startTimes.push_back( view->item_at_index(i)->lastStart() );
    }
  }

  //view->stopAllTimers();
  // delete old tasks
  while (view->item_at_index(0)) view->item_at_index(0)->cut();

  // 1. insert tasks form rc into taskview
  // 1.1. Build dictionary to look up Task object from Todo uid.  Each task is a
  // TQListViewItem, and is initially added with the view as the parent.
  todoList = rc->rawTodos();
  for( todo = todoList.begin(); todo != todoList.end(); ++todo )
  {
    Task* task = new Task(*todo, view);
    map.insert( (*todo)->uid(), task );
    view->setRootIsDecorated(true);
    task->setPixmapProgress();
  }

  // 1.1. Load each task under it's parent task.
  for( todo = todoList.begin(); todo != todoList.end(); ++todo )
  {
    Task* task = map.find( (*todo)->uid() );

    // No relatedTo incident just means this is a top-level task.
    if ( (*todo)->relatedTo() )
    {
      Task* newParent = map.find( (*todo)->relatedToUid() );

      // Complete the loading but return a message
      if ( !newParent )
        err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
          .arg(task->name())
          .arg((*todo)->relatedToUid());

      if (!err) task->move( newParent);
    }
  }

  view->clearActiveTasks();
  // restart tasks that have been running with their start times
  for ( int i=0; i<view->count(); i++)
  {
    for ( unsigned int n=0; n<runningTasks.size(); n++)
    {
      if ( runningTasks[n] == view->item_at_index(i)->uid() )
      {
        view->startTimerFor( view->item_at_index(i), startTimes[n] ); 
      }
    }
  }
  
  view->refresh();

  return err;
}

void KarmStorage::closeStorage(TaskView* view)
{
  if ( _calendar )
  {
    _calendar->close();
    delete _calendar;
    _calendar = 0;

    view->clear();
  }
}

TQString KarmStorage::save(TaskView* taskview)
{
  kdDebug(5970) << "entering KarmStorage::save" << endl;
  TQString err=TQString();

  TQPtrStack< KCal::Todo > parents;

  for (Task* task=taskview->first_child(); task; task = task->nextSibling())
  {
    err=writeTaskAsTodo(task, 1, parents );
  }

  if ( !saveCalendar() )
  {
    err="Could not save";
  }

  if ( err.isEmpty() )
  {
    kdDebug(5970)
      << "KarmStorage::save : wrote "
      << taskview->count() << " tasks to " << _icalfile << endl;
  }
  else
  {
    kdWarning(5970) << "KarmStorage::save : " << err << endl;
  }

  return err;
}

TQString KarmStorage::writeTaskAsTodo(Task* task, const int level,
    TQPtrStack< KCal::Todo >& parents )
{
  TQString err;
  KCal::Todo* todo;

  todo = _calendar->todo(task->uid());
  if ( !todo )
  {
    kdDebug(5970) << "Could not get todo from calendar" << endl;
    return "Could not get todo from calendar";
  }
  task->asTodo(todo);
  if ( !parents.isEmpty() ) todo->setRelatedTo( parents.top() );
  parents.push( todo );

  for ( Task* nextTask = task->firstChild(); nextTask;
        nextTask = nextTask->nextSibling() )
  {
    err = writeTaskAsTodo(nextTask, level+1, parents );
  }

  parents.pop();
  return err;
}

bool KarmStorage::isEmpty()
{
  KCal::Todo::List todoList;

  todoList = _calendar->rawTodos();
  return todoList.empty();
}

bool KarmStorage::isNewStorage(const Preferences* preferences) const
{
  if ( !_icalfile.isNull() ) return preferences->iCalFile() != _icalfile;
  else return false;
}

//----------------------------------------------------------------------------
// Routines that handle legacy flat file format.
// These only stored total and session times.
//

TQString KarmStorage::loadFromFlatFile(TaskView* taskview,
    const TQString& filename)
{
  TQString err;

  kdDebug(5970)
    << "KarmStorage::loadFromFlatFile: " << filename << endl;

  TQFile f(filename);
  if( !f.exists() )
    err = i18n("File \"%1\" not found.").arg(filename);

  if (!err)
  {
    if( !f.open( IO_ReadOnly ) )
      err = i18n("Could not open \"%1\".").arg(filename);
  }

  if (!err)
  {

    TQString line;

    TQPtrStack<Task> stack;
    Task *task;

    TQTextStream stream(&f);

    while( !stream.atEnd() ) {
      // lukas: this breaks for non-latin1 chars!!!
      // if ( file.readLine( line, T_LINESIZE ) == 0 )
      //   break;

      line = stream.readLine();
      kdDebug(5970) << "DEBUG: line: " << line << "\n";

      if (line.isNull())
        break;

      long minutes;
      int level;
      TQString name;
      DesktopList desktopList;
      if (!parseLine(line, &minutes, &name, &level, &desktopList))
        continue;

      unsigned int stackLevel = stack.count();
      for (unsigned int i = level; i<=stackLevel ; i++) {
        stack.pop();
      }

      if (level == 1) {
        kdDebug(5970) << "KarmStorage::loadFromFlatFile - toplevel task: "
          << name << " min: " << minutes << "\n";
        task = new Task(name, minutes, 0, desktopList, taskview);
        task->setUid(addTask(task, 0));
      }
      else {
        Task *parent = stack.top();
        kdDebug(5970) << "KarmStorage::loadFromFlatFile - task: " << name
            << " min: " << minutes << " parent" << parent->name() << "\n";
        task = new Task(name, minutes, 0, desktopList, parent);

        task->setUid(addTask(task, parent));

        // Legacy File Format (!):
        parent->changeTimes(0, -minutes);
        taskview->setRootIsDecorated(true);
        parent->setOpen(true);
      }
      if (!task->uid().isNull())
        stack.push(task);
      else
        delete task;
    }

    f.close();

  }

  return err;
}

TQString KarmStorage::loadFromFlatFileCumulative(TaskView* taskview,
    const TQString& filename)
{
  TQString err = loadFromFlatFile(taskview, filename);
  if (!err)
  {
    for (Task* task = taskview->first_child(); task;
        task = task->nextSibling())
    {
      adjustFromLegacyFileFormat(task);
    }
  }
  return err;
}

bool KarmStorage::parseLine(TQString line, long *time, TQString *name,
    int *level, DesktopList* desktopList)
{
  if (line.find('#') == 0) {
    // A comment line
    return false;
  }

  int index = line.find('\t');
  if (index == -1) {
    // This doesn't seem like a valid record
    return false;
  }

  TQString levelStr = line.left(index);
  TQString rest = line.remove(0,index+1);

  index = rest.find('\t');
  if (index == -1) {
    // This doesn't seem like a valid record
    return false;
  }

  TQString timeStr = rest.left(index);
  rest = rest.remove(0,index+1);

  bool ok;

  index = rest.find('\t'); // check for optional desktops string
  if (index >= 0) {
    *name = rest.left(index);
    TQString deskLine = rest.remove(0,index+1);

    // now transform the ds string (e.g. "3", or "1,4,5") into
    // an DesktopList
    TQString ds;
    int d;
    int commaIdx = deskLine.find(',');
    while (commaIdx >= 0) {
      ds = deskLine.left(commaIdx);
      d = ds.toInt(&ok);
      if (!ok)
        return false;

      desktopList->push_back(d);
      deskLine.remove(0,commaIdx+1);
      commaIdx = deskLine.find(',');
    }

    d = deskLine.toInt(&ok);

    if (!ok)
      return false;

    desktopList->push_back(d);
  }
  else {
    *name = rest.remove(0,index+1);
  }

  *time = timeStr.toLong(&ok);

  if (!ok) {
    // the time field was not a number
    return false;
  }
  *level = levelStr.toInt(&ok);
  if (!ok) {
    // the time field was not a number
    return false;
  }

  return true;
}

void KarmStorage::adjustFromLegacyFileFormat(Task* task)
{
  // unless the parent is the listView
  if ( task->parent() )
    task->parent()->changeTimes(-task->sessionTime(), -task->time());

  // traverse depth first -
  // as soon as we're in a leaf, we'll substract it's time from the parent
  // then, while descending back we'll do the same for each node untill
  // we reach the root
  for ( Task* subtask = task->firstChild(); subtask;
      subtask = subtask->nextSibling() )
    adjustFromLegacyFileFormat(subtask);
}

//----------------------------------------------------------------------------
// Routines that handle Comma-Separated Values export file format.
//
TQString KarmStorage::exportcsvFile( TaskView *taskview,
                                    const ReportCriteria &rc )
{
  TQString delim = rc.delimiter;
  TQString dquote = rc.quote;
  TQString double_dquote = dquote + dquote;
  bool to_quote = true;

  TQString err;
  Task* task;
  int maxdepth=0;

  kdDebug(5970)
    << "KarmStorage::exportcsvFile: " << rc.url << endl;

  TQString title = i18n("Export Progress");
  KProgressDialog dialog( taskview, 0, title );
  dialog.setAutoClose( true );
  dialog.setAllowCancel( true );
  dialog.progressBar()->setTotalSteps( 2 * taskview->count() );

  // The default dialog was not displaying all the text in the title bar.
  int width = taskview->fontMetrics().width(title) * 3;
  TQSize dialogsize;
  dialogsize.setWidth(width);
  dialog.setInitialSize( dialogsize, true );

  if ( taskview->count() > 1 ) dialog.show();

  TQString retval;

  // Find max task depth
  int tasknr = 0;
  while ( tasknr < taskview->count() && !dialog.wasCancelled() )
  {
    dialog.progressBar()->advance( 1 );
    if ( tasknr % 15 == 0 ) kapp->processEvents(); // repainting is slow
    if ( taskview->item_at_index(tasknr)->depth() > maxdepth )
      maxdepth = taskview->item_at_index(tasknr)->depth();
    tasknr++;
  }

  // Export to file
  tasknr = 0;
  while ( tasknr < taskview->count() && !dialog.wasCancelled() )
  {
    task = taskview->item_at_index( tasknr );
    dialog.progressBar()->advance( 1 );
    if ( tasknr % 15 == 0 ) kapp->processEvents();

    // indent the task in the csv-file:
    for ( int i=0; i < task->depth(); ++i ) retval += delim;

    /*
    // CSV compliance
    // Surround the field with quotes if the field contains
    // a comma (delim) or a double quote
    if (task->name().contains(delim) || task->name().contains(dquote))
      to_quote = true;
    else
      to_quote = false;
    */
    to_quote = true;

    if (to_quote)
      retval += dquote;

    // Double quotes replaced by a pair of consecutive double quotes
    retval += task->name().replace( dquote, double_dquote );

    if (to_quote)
      retval += dquote;

    // maybe other tasks are more indented, so to align the columns:
    for ( int i = 0; i < maxdepth - task->depth(); ++i ) retval += delim;

    retval += delim + formatTime( task->sessionTime(),
                                   rc.decimalMinutes )
           + delim + formatTime( task->time(),
                                   rc.decimalMinutes )
           + delim + formatTime( task->totalSessionTime(),
                                   rc.decimalMinutes )
           + delim + formatTime( task->totalTime(),
                                   rc.decimalMinutes )
           + "\n";
    tasknr++;
  }

  // save, either locally or remote
  if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
  {
    TQString filename=rc.url.path();
    if (filename.isEmpty()) filename=rc.url.url();
    TQFile f( filename );
    if( !f.open( IO_WriteOnly ) ) {
        err = i18n( "Could not open \"%1\"." ).arg( filename );
    }
    if (!err)
    {
      TQTextStream stream(&f);
      // Export to file
      stream << retval;
      f.close();
    }
  }
  else // use remote file
  {
    KTempFile tmpFile;
    if ( tmpFile.status() != 0 ) err = TQString::fromLatin1( "Unable to get temporary file" );
    else
    {
      TQTextStream *stream=tmpFile.textStream();
      *stream << retval;
      tmpFile.close();
      if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=TQString::fromLatin1("Could not upload");
    }
  }

  return err;
}

//----------------------------------------------------------------------------
// Routines that handle logging KArm history
//

//
// public routines:
//

TQString KarmStorage::addTask(const Task* task, const Task* parent)
{
  KCal::Todo* todo;
  TQString uid;

  todo = new KCal::Todo();
  if ( _calendar->addTodo( todo ) )
  {
    task->asTodo( todo  );
    if (parent)
      todo->setRelatedTo(_calendar->todo(parent->uid()));
    uid = todo->uid();
  }
  else
  {
    // Most likely a lock could not be pulled, although there are other
    // possiblities (like a really confused resource manager).
    uid = "";
  }

  return uid;
}

bool KarmStorage::removeTask(Task* task)
{

  // delete history
  KCal::Event::List eventList = _calendar->rawEvents();
  for(KCal::Event::List::iterator i = eventList.begin();
      i != eventList.end();
      ++i)
  {
    //kdDebug(5970) << "KarmStorage::removeTask: "
    //  << (*i)->uid() << " - relatedToUid() "
    //  << (*i)->relatedToUid()
    //  << ", relatedTo() = " << (*i)->relatedTo() <<endl;
    if ( (*i)->relatedToUid() == task->uid()
        || ( (*i)->relatedTo()
            && (*i)->relatedTo()->uid() == task->uid()))
    {
      _calendar->deleteEvent(*i);
    }
  }

  // delete todo
  KCal::Todo *todo = _calendar->todo(task->uid());
  _calendar->deleteTodo(todo);

  // Save entire file
  saveCalendar();

  return true;
}

void KarmStorage::addComment(const Task* task, const TQString& comment)
{

  KCal::Todo* todo;

  todo = _calendar->todo(task->uid());

  // Do this to avoid compiler warnings about comment not being used.  once we
  // transition to using the addComment method, we need this second param.
  TQString s = comment;

  // TODO: Use libkcal comments
  // todo->addComment(comment);
  // temporary
  todo->setDescription(task->comment());

  saveCalendar();
}

long KarmStorage::printTaskHistory (
        const Task               *task,
        const TQMap<TQString,long> &taskdaytotals,
        TQMap<TQString,long>       &daytotals,
        const TQDate              &from,
        const TQDate              &to,
        const int                level,
	vector <TQString>         &matrix,
        const ReportCriteria     &rc)
// to>=from is precondition
{
  long ownline=linenr++; // the how many-th instance of this function is this
  long colrectot=0;      // colum where to write the task's total recursive time
  vector <TQString> cell; // each line of the matrix is stored in an array of cells, one containing the recursive total
  long add;              // total recursive time of all subtasks
  TQString delim = rc.delimiter;
  TQString dquote = rc.quote;
  TQString double_dquote = dquote + dquote;
  bool to_quote = true;

  const TQString cr = TQString::fromLatin1("\n");
  TQString buf;
  TQString daytaskkey, daykey;
  TQDate day;
  long sum;

  if ( !task ) return 0;

  day = from;
  sum = 0;
  while (day <= to)
  {
    // write the time in seconds for the given task for the given day to s
    daykey = day.toString(TQString::fromLatin1("yyyyMMdd"));
    daytaskkey = TQString::fromLatin1("%1_%2")
      .arg(daykey)
      .arg(task->uid());

    if (taskdaytotals.contains(daytaskkey))
    {
      cell.push_back(TQString::fromLatin1("%1")
        .arg(formatTime(taskdaytotals[daytaskkey]/60, rc.decimalMinutes)));
      sum += taskdaytotals[daytaskkey];  // in seconds

      if (daytotals.contains(daykey))
        daytotals.replace(daykey, daytotals[daykey]+taskdaytotals[daytaskkey]);
      else
        daytotals.insert(daykey, taskdaytotals[daytaskkey]);
    }
    cell.push_back(delim);

    day = day.addDays(1);
  }

  // Total for task
  cell.push_back(TQString::fromLatin1("%1").arg(formatTime(sum/60, rc.decimalMinutes)));

  // room for the recursive total time (that cannot be calculated now)
  cell.push_back(delim);
  colrectot = cell.size();
  cell.push_back("???");
  cell.push_back(delim);

  // Task name
  for ( int i = level + 1; i > 0; i-- ) cell.push_back(delim);

  /*
  // CSV compliance
  // Surround the field with quotes if the field contains
  // a comma (delim) or a double quote
  to_quote = task->name().contains(delim) || task->name().contains(dquote);
  */
  to_quote = true;
  if ( to_quote) cell.push_back(dquote);


  // Double quotes replaced by a pair of consecutive double quotes
  cell.push_back(task->name().replace( dquote, double_dquote ));

  if ( to_quote) cell.push_back(dquote);

  cell.push_back(cr);

  add=0;
  for (Task* subTask = task->firstChild();
      subTask;
      subTask = subTask->nextSibling())
  {
    add += printTaskHistory( subTask, taskdaytotals, daytotals, from, to , level+1, matrix,
                      rc );
  }
  cell[colrectot]=(TQString::fromLatin1("%1").arg(formatTime((add+sum)/60, rc.decimalMinutes )));
  for (unsigned int i=0; i < cell.size(); i++) matrix[ownline]+=cell[i];
  return add+sum;
}

TQString KarmStorage::report( TaskView *taskview, const ReportCriteria &rc )
{
  TQString err;
  if ( rc.reportType == ReportCriteria::CSVHistoryExport )
      err = exportcsvHistory( taskview, rc.from, rc.to, rc );
  else if ( rc.reportType == ReportCriteria::CSVTotalsExport )
      err = exportcsvFile( taskview, rc );
  else {
      // hmmmm ... assert(0)?
  }
  return err;
}

// export history report as csv, all tasks X all dates in one block
TQString KarmStorage::exportcsvHistory ( TaskView      *taskview,
                                            const TQDate   &from,
                                            const TQDate   &to,
                                            const ReportCriteria &rc)
{
  TQString delim = rc.delimiter;
  const TQString cr = TQString::fromLatin1("\n");
  TQString err;

  // below taken from timekard.cpp
  TQString retval;
  TQString taskhdr, totalhdr;
  TQString line, buf;
  long sum;

  TQValueList<HistoryEvent> events;
  TQValueList<HistoryEvent>::iterator event;
  TQMap<TQString, long> taskdaytotals;
  TQMap<TQString, long> daytotals;
  TQString daytaskkey, daykey;
  TQDate day;
  TQDate dayheading;

  // parameter-plausi
  if ( from > to )
  {
    err = TQString::fromLatin1 (
            "'to' has to be a date later than or equal to 'from'.");
  }

  // header
  retval += i18n("Task History\n");
  retval += i18n("From %1 to %2")
    .arg(KGlobal::locale()->formatDate(from))
    .arg(KGlobal::locale()->formatDate(to));
  retval += cr;
  retval += i18n("Printed on: %1")
    .arg(KGlobal::locale()->formatDateTime(TQDateTime::currentDateTime()));
  retval += cr;

  day=from;
  events = taskview->getHistory(from, to);
  taskdaytotals.clear();
  daytotals.clear();

  // Build lookup dictionary used to output data in table cells.  keys are
  // in this format: YYYYMMDD_NNNNNN, where Y = year, M = month, d = day and
  // NNNNN = the VTODO uid.  The value is the total seconds logged against
  // that task on that day.  Note the UID is the todo id, not the event id,
  // so times are accumulated for each task.
  for (event = events.begin(); event != events.end(); ++event)
  {
    daykey = (*event).start().date().toString(TQString::fromLatin1("yyyyMMdd"));
    daytaskkey = TQString(TQString::fromLatin1("%1_%2"))
        .arg(daykey)
        .arg((*event).todoUid());

    if (taskdaytotals.contains(daytaskkey))
        taskdaytotals.replace(daytaskkey,
                taskdaytotals[daytaskkey] + (*event).duration());
    else
        taskdaytotals.insert(daytaskkey, (*event).duration());
  }

  // day headings
  dayheading = from;
  while ( dayheading <= to )
  {
    // Use ISO 8601 format for date.
    retval += dayheading.toString(TQString::fromLatin1("yyyy-MM-dd"));
    retval += delim;
    dayheading=dayheading.addDays(1);
  }
  retval += i18n("Sum") + delim + i18n("Total Sum") + delim + i18n("Task Hierarchy");
  retval += cr;
  retval += line;

  // the tasks
  vector <TQString> matrix;
  linenr=0;
  for (int i=0; i<=taskview->count()+1; i++) matrix.push_back("");
  if (events.empty())
  {
    retval += i18n("  No hours logged.");
  }
  else
  {
    if ( rc.allTasks )
    {
      for ( Task* task= taskview->item_at_index(0);
            task; task= task->nextSibling() )
      {
        printTaskHistory( task, taskdaytotals, daytotals, from, to, 0,
                          matrix, rc );
      }
    }
    else
    {
      printTaskHistory( taskview->current_item(), taskdaytotals, daytotals,
                        from, to, 0, matrix, rc );
    }
    for (unsigned int i=0; i<matrix.size(); i++) retval+=matrix[i];
    retval += line;

    // totals
    sum = 0;
    day = from;
    while (day<=to)
    {
      daykey = day.toString(TQString::fromLatin1("yyyyMMdd"));

      if (daytotals.contains(daykey))
      {
        retval += TQString::fromLatin1("%1")
            .arg(formatTime(daytotals[daykey]/60, rc.decimalMinutes));
        sum += daytotals[daykey];  // in seconds
      }
      retval += delim;
      day = day.addDays(1);
    }

    retval += TQString::fromLatin1("%1%2%3%4")
        .arg( formatTime( sum/60, rc.decimalMinutes ) )
        .arg( delim ).arg( delim )
        .arg( i18n( "Total" ) );
  }

  // above taken from timekard.cpp

  // save, either locally or remote

  if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
  {
    TQString filename=rc.url.path();
    if (filename.isEmpty()) filename=rc.url.url();
    TQFile f( filename );
    if( !f.open( IO_WriteOnly ) ) {
        err = i18n( "Could not open \"%1\"." ).arg( filename );
    }
    if (!err)
    {
      TQTextStream stream(&f);
      // Export to file
      stream << retval;
      f.close();
    }
  }
  else // use remote file
  {
    KTempFile tmpFile;
    if ( tmpFile.status() != 0 )
    {
      err = TQString::fromLatin1( "Unable to get temporary file" );
    }
    else
    {
      TQTextStream *stream=tmpFile.textStream();
      *stream << retval;
      tmpFile.close();
      if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=TQString::fromLatin1("Could not upload");
    }
  }
  return err;
}

void KarmStorage::stopTimer(const Task* task, TQDateTime when)
{
  kdDebug(5970) << "Entering KarmStorage::stopTimer" << endl;
  long delta = task->startTime().secsTo(when);
  changeTime(task, delta);
}

bool KarmStorage::bookTime(const Task* task,
                           const TQDateTime& startDateTime,
                           const long durationInSeconds)
{
  // Ignores preferences setting re: logging history.
  KCal::Event* e;
  TQDateTime end;

  e = baseEvent( task );
  e->setDtStart( startDateTime );
  e->setDtEnd( startDateTime.addSecs( durationInSeconds ) );

  // Use a custom property to keep a record of negative durations
  e->setCustomProperty( kapp->instanceName(),
      TQCString("duration"),
      TQString::number(durationInSeconds));

  return _calendar->addEvent(e);
}

void KarmStorage::changeTime(const Task* task, const long deltaSeconds)
{
  kdDebug(5970) << "Entering KarmStorage::changeTime ( " << task->name() << "," << deltaSeconds <<  " )" << endl;
  KCal::Event* e;
  TQDateTime end;

  // Don't write events (with timer start/stop duration) if user has turned
  // this off in the settings dialog.
  if ( ! task->taskView()->preferences()->logging() ) return;

  e = baseEvent(task);

  // Don't use duration, as ICalFormatImpl::writeIncidence never writes a
  // duration, even though it looks like it's used in event.cpp.
  end = task->startTime();
  if ( deltaSeconds > 0 ) end = task->startTime().addSecs(deltaSeconds);
  e->setDtEnd(end);

  // Use a custom property to keep a record of negative durations
  e->setCustomProperty( kapp->instanceName(),
      TQCString("duration"),
      TQString::number(deltaSeconds));

  _calendar->addEvent(e);

  // This saves the entire iCal file each time, which isn't efficient but
  // ensures no data loss.  A faster implementation would be to append events
  // to a file, and then when KArm closes, append the data in this file to the
  // iCal file.
  //
  // Meanwhile, we simply use a timer to delay the full-saving until the GUI
  // has updated, for better user feedback. Feel free to get rid of this
  // if/when implementing the faster saving (DF).
  task->taskView()->scheduleSave();
}


KCal::Event* KarmStorage::baseEvent(const Task * task)
{
  KCal::Event* e;
  TQStringList categories;

  e = new KCal::Event;
  e->setSummary(task->name());

  // Can't use setRelatedToUid()--no error, but no RelatedTo written to disk
  e->setRelatedTo(_calendar->todo(task->uid()));

  // Debugging: some events where not getting a related-to field written.
  assert(e->relatedTo()->uid() == task->uid());

  // Have to turn this off to get datetimes in date fields.
  e->setFloats(false);
  e->setDtStart(task->startTime());

  // So someone can filter this mess out of their calendar display
  categories.append(i18n("KArm"));
  e->setCategories(categories);

  return e;
}

HistoryEvent::HistoryEvent(TQString uid, TQString name, long duration,
        TQDateTime start, TQDateTime stop, TQString todoUid)
{
  _uid = uid;
  _name = name;
  _duration = duration;
  _start = start;
  _stop = stop;
  _todoUid = todoUid;
}


TQValueList<HistoryEvent> KarmStorage::getHistory(const TQDate& from,
    const TQDate& to)
{
  TQValueList<HistoryEvent> retval;
  TQStringList processed;
  KCal::Event::List events;
  KCal::Event::List::iterator event;
  TQString duration;

  for(TQDate d = from; d <= to; d = d.addDays(1))
  {
    events = _calendar->rawEventsForDate( d );
    for (event = events.begin(); event != events.end(); ++event)
    {

      // KArm events have the custom property X-TDE-Karm-duration
      if (! processed.contains( (*event)->uid()))
      {
        // If an event spans multiple days, CalendarLocal::rawEventsForDate
        // will return the same event on both days.  To avoid double-counting
        // such events, we (arbitrarily) attribute the hours from both days on
        // the first day.  This mis-reports the actual time spent, but it is
        // an easy fix for a (hopefully) rare situation.
        processed.append( (*event)->uid());

        duration = (*event)->customProperty(kapp->instanceName(),
            TQCString("duration"));
        if ( ! duration.isNull() )
        {
          if ( (*event)->relatedTo()
              &&  ! (*event)->relatedTo()->uid().isEmpty() )
          {
            retval.append(HistoryEvent(
                (*event)->uid(),
                (*event)->summary(),
                duration.toLong(),
                (*event)->dtStart(),
                (*event)->dtEnd(),
                (*event)->relatedTo()->uid()
                ));
          }
          else
            // Something is screwy with the ics file, as this KArm history event
            // does not have a todo related to it.  Could have been deleted
            // manually?  We'll continue with report on with report ...
            kdDebug(5970) << "KarmStorage::getHistory(): "
              << "The event " << (*event)->uid()
              << " is not related to a todo.  Dropped." << endl;
        }
      }
    }
  }

  return retval;
}

bool KarmStorage::remoteResource( const TQString& file ) const
{
  TQString f = file.lower();
  bool rval = f.startsWith( "http://" ) || f.startsWith( "ftp://" );

  kdDebug(5970) << "KarmStorage::remoteResource( " << file << " ) returns " << rval  << endl;
  return rval;
}

bool KarmStorage::saveCalendar()
{
  kdDebug(5970) << "KarmStorage::saveCalendar" << endl;

#if 0
  Event::List evl=_calendar->rawEvents();
  kdDebug(5970) << "summary - dtStart - dtEnd" << endl;
  for (unsigned int i=0; i<evl.count(); i++) 
  {
    kdDebug() << evl[i]->summary() << evl[i]->dtStart() << evl[i]->dtEnd() << endl;
  }
#endif
  KABC::Lock *lock = _calendar->lock();
  if ( !lock || !lock->lock() )
    return false;

  if ( _calendar && _calendar->save() ) {
    lock->unlock();
    return true;
  }

  lock->unlock();
  return false;
}