////////////////////////////////////////////////////////////////////////////////
// taskwarrior - a command line task list manager.
//
// Copyright 2006-2012, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// http://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////

#define L10N                                           // Localization complete.

#include <iostream>
#include <sstream>
#include <stdlib.h>
#include <unistd.h>
#include <Duration.h>
#include <Context.h>
#include <i18n.h>
#include <text.h>
#include <util.h>
#include <i18n.h>
#include <main.h>
#include <CmdEdit.h>

extern Context context;

////////////////////////////////////////////////////////////////////////////////
CmdEdit::CmdEdit ()
{
  _keyword     = "edit";
  _usage       = "task <filter> edit";
  _description = STRING_CMD_EDIT_USAGE;
  _read_only   = false;
  _displays_id = true;
}

////////////////////////////////////////////////////////////////////////////////
// Introducing the Silver Bullet.  This feature is the catch-all fixative for
// various other ills.  This is like opening up the hood and going in with a
// wrench.  To be used sparingly.
int CmdEdit::execute (std::string& output)
{
  int rc = 0;

  // Filter the tasks.
  handleRecurrence ();
  std::vector <Task> filtered;
  filter (filtered);

  // Find number of matching tasks.  Skip recurring parent tasks.
  std::vector <Task>::iterator task;
  for (task = filtered.begin (); task != filtered.end (); ++task)
    if (editFile (*task))
      context.tdb2.modify (*task);

  context.tdb2.commit ();
  return rc;
}

////////////////////////////////////////////////////////////////////////////////
std::string CmdEdit::findValue (
  const std::string& text,
  const std::string& name)
{
  std::string::size_type found = text.find (name);
  if (found != std::string::npos)
  {
    std::string::size_type eol = text.find ("\n", found + 1);
    if (eol != std::string::npos)
    {
      std::string value = text.substr (
        found + name.length (),
        eol - (found + name.length ()));

      return trim (value, "\t ");
    }
  }

  return "";
}

////////////////////////////////////////////////////////////////////////////////
std::string CmdEdit::formatDate (
  Task& task,
  const std::string& attribute,
  const std::string& dateformat)
{
  std::string value = task.get (attribute);
  if (value.length ())
  {
    Date dt (strtol (value.c_str (), NULL, 10));
    value = dt.toString (dateformat);
  }

  return value;
}

////////////////////////////////////////////////////////////////////////////////
std::string CmdEdit::formatTask (Task task, const std::string& dateformat)
{
  std::stringstream before;
  bool verbose = context.verbose ("edit") ||
                 context.config.getBoolean ("edit.verbose"); // Deprecated 2.0

  if (verbose)
    before << "# " << STRING_EDIT_HEADER_1 << "\n"
           << "# " << STRING_EDIT_HEADER_2 << "\n"
           << "# " << STRING_EDIT_HEADER_3 << "\n"
           << "# " << STRING_EDIT_HEADER_4 << "\n"
           << "# " << STRING_EDIT_HEADER_5 << "\n"
           << "# " << STRING_EDIT_HEADER_6 << "\n"
           << "#\n"
           << "# " << STRING_EDIT_HEADER_7 << "\n"
           << "# " << STRING_EDIT_HEADER_8 << "\n"
           << "# " << STRING_EDIT_HEADER_9 << "\n"
           << "#\n"
           << "# " << STRING_EDIT_HEADER_10 << "\n"
           << "# " << STRING_EDIT_HEADER_11 << "\n"
           << "# " << STRING_EDIT_HEADER_12 << "\n"
           << "#\n";

  before << "# " << STRING_EDIT_TABLE_HEADER_1 << "\n"
         << "# " << STRING_EDIT_TABLE_HEADER_2 << "\n"
         << "# ID:                " << task.id                                          << "\n"
         << "# UUID:              " << task.get ("uuid")                                << "\n"
         << "# Status:            " << ucFirst (Task::statusToText (task.getStatus ())) << "\n"
         << "# Mask:              " << task.get ("mask")                                << "\n"
         << "# iMask:             " << task.get ("imask")                               << "\n"
         << "  Project:           " << task.get ("project")                             << "\n"
         << "  Priority:          " << task.get ("priority")                            << "\n";

  std::vector <std::string> tags;
  task.getTags (tags);
  std::string allTags;
  join (allTags, " ", tags);

  if (verbose)
    before << "# " << STRING_EDIT_TAG_SEP << "\n";

  before << "  Tags:              " << allTags                                          << "\n"
         << "  Description:       " << task.get ("description")                         << "\n"
         << "  Created:           " << formatDate (task, "entry", dateformat)           << "\n"
         << "  Started:           " << formatDate (task, "start", dateformat)           << "\n"
         << "  Ended:             " << formatDate (task, "end", dateformat)             << "\n"
         << "  Scheduled:         " << formatDate (task, "scheduled", dateformat)       << "\n"
         << "  Due:               " << formatDate (task, "due", dateformat)             << "\n"
         << "  Until:             " << formatDate (task, "until", dateformat)           << "\n"
         << "  Recur:             " << task.get ("recur")                               << "\n"
         << "  Wait until:        " << formatDate (task, "wait", dateformat)            << "\n"
         << "  Parent:            " << task.get ("parent")                              << "\n"
         << "  Foreground color:  " << task.get ("fg")                                  << "\n"
         << "  Background color:  " << task.get ("bg")                                  << "\n";

  if (verbose)
    before << "# " << STRING_EDIT_HEADER_13 << "\n"
           << "# " << STRING_EDIT_HEADER_14 << "\n"
           << "# " << STRING_EDIT_HEADER_15 << "\n";

  std::map <std::string, std::string> annotations;
  task.getAnnotations (annotations);
  std::map <std::string, std::string>::iterator anno;
  for (anno = annotations.begin (); anno != annotations.end (); ++anno)
  {
    Date dt (strtol (anno->first.substr (11).c_str (), NULL, 10));
    before << "  Annotation:        " << dt.toString (dateformat)
           << " -- "                  << anno->second << "\n";
  }

  Date now;
  before << "  Annotation:        " << now.toString (dateformat) << " -- \n";

  // Add dependencies here.
  std::vector <std::string> dependencies;
  task.getDependencies (dependencies);
  std::stringstream allDeps;
  for (unsigned int i = 0; i < dependencies.size (); ++i)
  {
    if (i)
      allDeps << ",";

    Task t;
    context.tdb2.get (dependencies[i], t);
    if (t.getStatus () == Task::pending ||
        t.getStatus () == Task::waiting)
      allDeps << t.id;
    else
      allDeps << dependencies[i];
  }

  if (verbose)
    before << "# " << STRING_EDIT_DEP_SEP << "\n";

  before << "  Dependencies:      " << allDeps.str () << "\n";

  before << "# " << STRING_EDIT_END << "\n";
  return before.str ();
}

////////////////////////////////////////////////////////////////////////////////
void CmdEdit::parseTask (Task& task, const std::string& after, const std::string& dateformat)
{
  // project
  std::string value = findValue (after, "\n  Project:");
  if (task.get ("project") != value)
  {
    if (value != "")
    {
      context.footnote (STRING_EDIT_PROJECT_MOD);
      task.set ("project", value);
    }
    else
    {
      context.footnote (STRING_EDIT_PROJECT_DEL);
      task.remove ("project");
    }
  }

  // priority
  value = findValue (after, "\n  Priority:");
  if (task.get ("priority") != value)
  {
    if (value != "")
    {
      if (context.columns["priority"]->validate (value))
      {
        context.footnote (STRING_EDIT_PRIORITY_MOD);
        task.set ("priority", value);
      }
    }
    else
    {
      context.footnote (STRING_EDIT_PRIORITY_DEL);
      task.remove ("priority");
    }
  }

  // tags
  value = findValue (after, "\n  Tags:");
  std::vector <std::string> tags;
  split (tags, value, ' ');
  task.remove ("tags");
  task.addTags (tags);

  // description.
  value = findValue (after, "\n  Description:");
  if (task.get ("description") != value)
  {
    if (value != "")
    {
      context.footnote (STRING_EDIT_DESC_MOD);
      task.set ("description", value);
    }
    else
      throw std::string (STRING_EDIT_DESC_REMOVE_ERR);
  }

  // entry
  value = findValue (after, "\n  Created:");
  if (value != "")
  {
    std::string formatted = formatDate (task, "entry", dateformat);

    if (formatted != value)
    {
      context.footnote (STRING_EDIT_ENTRY_MOD);
      task.set ("entry", Date(value, dateformat).toEpochString ());
    }
  }
  else
    throw std::string (STRING_EDIT_ENTRY_REMOVE_ERR);

  // start
  value = findValue (after, "\n  Started:");
  if (value != "")
  {
    if (task.get ("start") != "")
    {
      std::string formatted = formatDate (task, "start", dateformat);

      if (formatted != value)
      {
        context.footnote (STRING_EDIT_START_MOD);
        task.set ("start", Date(value, dateformat).toEpochString ());
      }
    }
    else
    {
      context.footnote (STRING_EDIT_START_MOD);
      task.set ("start", Date(value, dateformat).toEpochString ());
    }
  }
  else
  {
    if (task.get ("start") != "")
    {
      context.footnote (STRING_EDIT_START_DEL);
      task.remove ("start");
    }
  }

  // end
  value = findValue (after, "\n  Ended:");
  if (value != "")
  {
    if (task.get ("end") != "")
    {
      std::string formatted = formatDate (task, "end", dateformat);

      if (formatted != value)
      {
        context.footnote (STRING_EDIT_END_MOD);
        task.set ("end", Date(value, dateformat).toEpochString ());
      }
    }
    else if (task.getStatus () != Task::deleted)
      throw std::string (STRING_EDIT_END_SET_ERR);
  }
  else
  {
    if (task.get ("end") != "")
    {
      context.footnote (STRING_EDIT_END_DEL);
      task.setStatus (Task::pending);
      task.remove ("end");
    }
  }

  // scheduled
  value = findValue (after, "\n  Scheduled:");
  if (value != "")
  {
    if (task.get ("scheduled") != "")
    {
      std::string formatted = formatDate (task, "scheduled", dateformat);

      if (formatted != value)
      {
        context.footnote (STRING_EDIT_SCHED_MOD);
        task.set ("scheduled", Date(value, dateformat).toEpochString ());
      }
    }
    else
    {
      context.footnote (STRING_EDIT_SCHED_MOD);
      task.set ("scheduled", Date(value, dateformat).toEpochString ());
    }
  }
  else
  {
    if (task.get ("scheduled") != "")
    {
      context.footnote (STRING_EDIT_SCHED_DEL);
      task.setStatus (Task::pending);
      task.remove ("scheduled");
    }
  }

  // due
  value = findValue (after, "\n  Due:");
  if (value != "")
  {
    if (task.get ("due") != "")
    {
      std::string formatted = formatDate (task, "due", dateformat);

      if (formatted != value)
      {
        context.footnote (STRING_EDIT_DUE_MOD);
        task.set ("due", Date(value, dateformat).toEpochString ());
      }
    }
    else
    {
      context.footnote (STRING_EDIT_DUE_MOD);
      task.set ("due", Date(value, dateformat).toEpochString ());
    }
  }
  else
  {
    if (task.get ("due") != "")
    {
      if (task.getStatus () == Task::recurring ||
          task.get ("parent") != "")
      {
        context.footnote (STRING_EDIT_DUE_DEL_ERR);
      }
      else
      {
        context.footnote (STRING_EDIT_DUE_DEL);
        task.remove ("due");
      }
    }
  }

  // until
  value = findValue (after, "\n  Until:");
  if (value != "")
  {
    if (task.get ("until") != "")
    {
      std::string formatted = formatDate (task, "until", dateformat);

      if (formatted != value)
      {
        context.footnote (STRING_EDIT_UNTIL_MOD);
        task.set ("until", Date(value, dateformat).toEpochString ());
      }
    }
    else
    {
      context.footnote (STRING_EDIT_UNTIL_MOD);
      task.set ("until", Date(value, dateformat).toEpochString ());
    }
  }
  else
  {
    if (task.get ("until") != "")
    {
      context.footnote (STRING_EDIT_UNTIL_DEL);
      task.remove ("until");
    }
  }

  // recur
  value = findValue (after, "\n  Recur:");
  if (value != task.get ("recur"))
  {
    if (value != "")
    {
      Duration d;
      if (d.valid (value))
      {
        context.footnote (STRING_EDIT_RECUR_MOD);
        if (task.get ("due") != "")
        {
          task.set ("recur", value);
          task.setStatus (Task::recurring);
        }
        else
          throw std::string (STRING_EDIT_RECUR_DUE_ERR);
      }
      else
        throw std::string (STRING_EDIT_RECUR_ERR);
    }
    else
    {
      context.footnote (STRING_EDIT_RECUR_DEL);
      task.setStatus (Task::pending);
      task.remove ("recur");
      task.remove ("until");
      task.remove ("mask");
      task.remove ("imask");
    }
  }

  // wait
  value = findValue (after, "\n  Wait until:");
  if (value != "")
  {
    if (task.get ("wait") != "")
    {
      std::string formatted = formatDate (task, "wait", dateformat);

      if (formatted != value)
      {
        context.footnote (STRING_EDIT_WAIT_MOD);
        task.set ("wait", Date(value, dateformat).toEpochString ());
        task.setStatus (Task::waiting);
      }
    }
    else
    {
      context.footnote (STRING_EDIT_WAIT_MOD);
      task.set ("wait", Date(value, dateformat).toEpochString ());
      task.setStatus (Task::waiting);
    }
  }
  else
  {
    if (task.get ("wait") != "")
    {
      context.footnote (STRING_EDIT_WAIT_DEL);
      task.remove ("wait");
      task.setStatus (Task::pending);
    }
  }

  // parent
  value = findValue (after, "\n  Parent:");
  if (value != task.get ("parent"))
  {
    if (value != "")
    {
      context.footnote (STRING_EDIT_PARENT_MOD);
      task.set ("parent", value);
    }
    else
    {
      context.footnote (STRING_EDIT_PARENT_DEL);
      task.remove ("parent");
    }
  }

  // fg
  value = findValue (after, "\n  Foreground color:");
  if (value != task.get ("fg"))
  {
    if (value != "")
    {
      context.footnote (STRING_EDIT_FG_MOD);
      task.set ("fg", value);
    }
    else
    {
      context.footnote (STRING_EDIT_FG_DEL);
      task.remove ("fg");
    }
  }

  // bg
  value = findValue (after, "\n  Background color:");
  if (value != task.get ("bg"))
  {
    if (value != "")
    {
      context.footnote (STRING_EDIT_BG_MOD);
      task.set ("bg", value);
    }
    else
    {
      context.footnote (STRING_EDIT_BG_DEL);
      task.remove ("bg");
    }
  }

  // Annotations
  std::map <std::string, std::string> annotations;
  std::string::size_type found = 0;
  while ((found = after.find ("\n  Annotation:", found)) != std::string::npos)
  {
    found += 14;  // Length of "\n  Annotation:".

    std::string::size_type eol = after.find ("\n", found + 1);
    if (eol != std::string::npos)
    {
      std::string value = trim (after.substr (
        found,
        eol - found), "\t ");

      std::string::size_type gap = value.find (" -- ");
      if (gap != std::string::npos)
      {
        // TODO keeping the initial dates even if dateformat approximates them
        // is complex as finding the correspondence between each original line
        // and edited line may be impossible (bug #705). It would be simpler if
        // each annotation was put on a line with a distinguishable id (then
        // for each line: if the annotation is the same, then it is copied; if
        // the annotation is modified, then its original date may be kept; and
        // if there is no corresponding id, then a new unique date is created).
        Date when (value.substr (0, gap), dateformat);

        // This guarantees that if more than one annotation has the same date,
        // that the seconds will be different, thus unique, thus not squashed.
        // Bug #249
        when += (const int) annotations.size ();

        std::stringstream name;
        name << "annotation_" << when.toEpoch ();
        std::string text = trim (value.substr (gap + 4), "\t ");
        annotations.insert (std::make_pair (name.str (), text));
      }
    }
  }

  task.setAnnotations (annotations);

  // Dependencies
  value = findValue (after, "\n  Dependencies:");
  std::vector <std::string> dependencies;
  split (dependencies, value, ",");

  task.remove ("depends");
  std::vector <std::string>::iterator dep;
  for (dep = dependencies.begin (); dep != dependencies.end (); ++dep)
  {
    std::vector <int> ids;

    // Crude UUID check
    if (dep->length () == 36)
    {
      int id = context.tdb2.pending.id (*dep);
      ids.push_back (id);
    }
    else
      A3::extract_id (*dep, ids);

    std::vector <int>::iterator id;
    for (id = ids.begin (); id != ids.end(); id++)
      task.addDependency (*id);
  }
}

////////////////////////////////////////////////////////////////////////////////
bool CmdEdit::editFile (Task& task)
{
  // Check for file permissions.
  Directory location (context.config.get ("data.location"));
  if (! location.writable ())
    throw std::string (STRING_EDIT_UNWRITABLE);

  // Create a temp file name in data.location.
  std::stringstream file;
  file << "task." << getpid () << "." << task.id << ".task";
  std::string path = location._data + "/" + file.str ();

  // Determine the output date format, which uses a hierarchy of definitions.
  //   rc.dateformat.edit
  //   rc.dateformat
  std::string dateformat = context.config.get ("dateformat.edit");
  if (dateformat == "")
    dateformat = context.config.get ("dateformat");

  // Format the contents, T -> text, write to a file.
  std::string before = formatTask (task, dateformat);
  int ignored = chdir (location._data.c_str ());
  ++ignored; // Keep compiler quiet.
  File::write (file.str (), before);

  // Determine correct editor: .taskrc:editor > $VISUAL > $EDITOR > vi
  std::string editor = context.config.get ("editor");
  char* peditor = getenv ("VISUAL");
  if (editor == "" && peditor) editor = std::string (peditor);
  peditor = getenv ("EDITOR");
  if (editor == "" && peditor) editor = std::string (peditor);
  if (editor == "") editor = "vi";

  // Complete the command line.
  editor += " ";
  editor += "\"" + file.str () + "\"";

ARE_THESE_REALLY_HARMFUL:
  bool changes = false; // No changes made.

  // Launch the editor.
  std::cout << format (STRING_EDIT_LAUNCHING, editor) << "\n";
  if (-1 == system (editor.c_str ()))
    std::cout << STRING_EDIT_NO_EDITS << "\n";
  else
    std::cout << STRING_EDIT_COMPLETE << "\n";

  // Slurp file.
  std::string after;
  File::read (file.str (), after);

  // Update task based on what can be parsed back out of the file, but only
  // if changes were made.
  if (before != after)
  {
    std::cout << STRING_EDIT_CHANGES << "\n";
    std::string problem = "";
    bool oops = false;

    try
    {
      parseTask (task, after, dateformat);
    }

    catch (std::string& e)
    {
      problem = e;
      oops = true;
    }

    if (oops)
    {
      std::cout << STRING_ERROR_PREFIX << problem << "\n";

      // Preserve the edits.
      before = after;
      File::write (file.str (), before);

      if (confirm (STRING_EDIT_UNPARSEABLE))
        goto ARE_THESE_REALLY_HARMFUL;
    }
    else
      changes = true;
  }
  else
  {
    std::cout << STRING_EDIT_NO_CHANGES << "\n";
    changes = false;
  }

  // Cleanup.
  File::remove (file.str ());
  return changes;
}

////////////////////////////////////////////////////////////////////////////////
