// Copyright (C) 2002 Graydon Hoare <graydon@pobox.com>
//
// This program is made available under the GNU GPL version 2.0 or
// greater. See the accompanying file COPYING for details.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE.

#include "base.hh"
#include <iostream>
#include <utility>

#include "charset.hh"
#include "cmd.hh"
#include "revision.hh"
#include "constants.hh"
#include "app_state.hh"
#include "database.hh"
#include "project.hh"
#include "keys.hh"
#include "key_store.hh"
#include "work.hh"
#include "rev_height.hh"
#include "transforms.hh"

using std::cin;
using std::cout;
using std::make_pair;
using std::pair;
using std::set;
using std::string;

CMD_GROUP(db, "db", "", CMD_REF(database),
          N_("Deals with the database"),
          "");

CMD(db_init, "init", "", CMD_REF(db), "",
    N_("Initializes a database"),
    N_("Creates a new database file and initializes it."),
    options::opts::none)
{
  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  db.initialize();
}

CMD(db_info, "info", "", CMD_REF(db), "",
    N_("Shows information about the database"),
    "",
    options::opts::none)
{
  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  db.info(cout);
}

CMD(db_version, "version", "", CMD_REF(db), "",
    N_("Shows the database's version"),
    "",
    options::opts::none)
{
  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  db.version(cout);
}

CMD(db_dump, "dump", "", CMD_REF(db), "",
    N_("Dumps the contents of the database"),
    N_("Generates a list of SQL instructions that represent the whole "
       "contents of the database.  The resulting output is useful to later "
       "restore the database from a text file that serves as a backup."),
    options::opts::none)
{
  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  db.dump(cout);
}

CMD(db_load, "load", "", CMD_REF(db), "",
    N_("Loads the contents of the database"),
    N_("Reads a list of SQL instructions that regenerate the contents of "
       "the database.  This is supposed to be used in conjunction with the "
       "output generated by the 'dump' command."),
    options::opts::none)
{
  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  db.load(cin);
}

CMD(db_migrate, "migrate", "", CMD_REF(db), "",
    N_("Migrates the database to a newer schema"),
    N_("Updates the database's internal schema to the most recent one.  "
       "Needed to automatically resolve incompatibilities that may be "
       "introduced in newer versions of monotone."),
    options::opts::none)
{
  key_store keys(app);

  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  db.migrate(keys);
}

CMD(db_execute, "execute", "", CMD_REF(db), "",
    N_("Executes an SQL command on the database"),
    N_("Directly executes the given SQL command on the database"),
    options::opts::none)
{
  if (args.size() != 1)
    throw usage(execid);

  database db(app);
  db.debug(idx(args, 0)(), cout);
}

CMD(db_kill_rev_locally, "kill_rev_locally", "", CMD_REF(db), "ID",
    N_("Kills a revision from the local database"),
    "",
    options::opts::none)
{
  if (args.size() != 1)
    throw usage(execid);

  revision_id revid;

  database db(app);
  project_t project(db);
  complete(app.opts, app.lua, project, idx(args, 0)(), revid);

  // Check that the revision does not have any children
  std::set<revision_id> children;
  db.get_revision_children(revid, children);
  N(!children.size(),
    F("revision %s already has children. We cannot kill it.")
    % revid);

  // If we're executing this in a workspace, check if the workspace parent
  // revision is the one to kill. If so, write out the changes made in this
  // particular revision to _MTN/revision to allow the user redo his (fixed)
  // commit afterwards. Of course we can't do this at all if
  //
  // a) the user is currently not inside a workspace
  // b) the user has updated the current workspace to another revision already
  //    thus the working revision is no longer based on the revision we're
  //    trying to kill
  // c) there are uncomitted changes in the working revision of this workspace.
  //    this *eventually* could be handled with a workspace merge scenario, but
  //    is left out for now
  if (workspace::found)
    {
      workspace work(app);
      revision_t old_work_rev;
      work.get_work_rev(old_work_rev);

      for (edge_map::const_iterator i = old_work_rev.edges.begin();
           i != old_work_rev.edges.end(); i++)
        {
          if (edge_old_revision(i) != revid)
            continue;

          N(!work.has_changes(db),
            F("Cannot kill revision %s,\n"
              "because it would leave the current workspace in an invalid\n"
              "state, from which monotone cannot recover automatically since\n"
              "the workspace contains uncommitted changes.\n"
              "Consider updating your workspace to another revision first,\n"
              "before you try to kill this revision again.")
              % revid);

          P(F("applying changes from %s on the current workspace")
            % revid);

          revision_t new_work_rev;
          db.get_revision(revid, new_work_rev);
          new_work_rev.made_for = made_for_workspace;
          work.put_work_rev(new_work_rev);
          
          // extra paranoia... we _should_ never run this section twice
          // since a merged workspace would fail early with work.has_changes()
          break;
        }
    }

  db.delete_existing_rev_and_certs(revid);
}

CMD(db_kill_branch_certs_locally, "kill_branch_certs_locally", "", CMD_REF(db),
    "BRANCH",
    N_("Kills branch certificates from the local database"),
    "",
    options::opts::none)
{
  if (args.size() != 1)
    throw usage(execid);

  database db(app);
  db.delete_branch_named(cert_value(idx(args, 0)()));
}

CMD(db_kill_tag_locally, "kill_tag_locally", "", CMD_REF(db), "TAG",
    N_("Kills a tag from the local database"),
    "",
    options::opts::none)
{
  if (args.size() != 1)
    throw usage(execid);

  database db(app);
  db.delete_tag_named(cert_value(idx(args, 0)()));
}

CMD(db_check, "check", "", CMD_REF(db), "",
    N_("Does some sanity checks on the database"),
    N_("Ensures that the database is consistent by issuing multiple "
       "checks."),
    options::opts::none)
{
  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  check_db(db);
}

CMD(db_changesetify, "changesetify", "", CMD_REF(db), "",
    N_("Converts the database to the changeset format"),
    "",
    options::opts::none)
{
  database db(app);
  key_store keys(app);

  N(args.size() == 0,
    F("no arguments needed"));

  db.ensure_open_for_format_changes();
  db.check_is_not_rosterified();

  // early short-circuit to avoid failure after lots of work
  cache_user_key(app.opts, app.lua, db, keys);

  build_changesets_from_manifest_ancestry(db, keys, set<string>());
}

CMD(db_rosterify, "rosterify", "", CMD_REF(db), "",
    N_("Converts the database to the rosters format"),
    "",
    options::opts::drop_attr)
{
  database db(app);
  key_store keys(app);

  N(args.size() == 0,
    F("no arguments needed"));

  db.ensure_open_for_format_changes();
  db.check_is_not_rosterified();

  // early short-circuit to avoid failure after lots of work
  cache_user_key(app.opts, app.lua, db, keys);

  build_roster_style_revs_from_manifest_style_revs(db, keys,
                                                   app.opts.attrs_to_drop);
}

CMD(db_regenerate_caches, "regenerate_caches", "", CMD_REF(db), "",
    N_("Regenerates the caches stored in the database"),
    "",
    options::opts::none)
{
  N(args.size() == 0,
    F("no arguments needed"));

  database db(app);
  regenerate_caches(db);
}

CMD_HIDDEN(clear_epoch, "clear_epoch", "", CMD_REF(db), "BRANCH",
    N_("Clears the branch's epoch"),
    "",
    options::opts::none)
{
  if (args.size() != 1)
    throw usage(execid);

  database db(app);
  db.clear_epoch(branch_name(idx(args, 0)()));
}

CMD(db_set_epoch, "set_epoch", "", CMD_REF(db), "BRANCH EPOCH",
    N_("Sets the branch's epoch"),
    "",
    options::opts::none)
{
  if (args.size() != 2)
    throw usage(execid);

  N(idx(args, 1)().size() == constants::epochlen,
    F("The epoch must be %s characters") % constants::epochlen);

  epoch_data ed(decode_hexenc(idx(args, 1)()));
  database db(app);
  db.set_epoch(branch_name(idx(args, 0)()), ed);
}

CMD(set, "set", "", CMD_REF(variables), N_("DOMAIN NAME VALUE"),
    N_("Sets a database variable"),
    N_("This command modifies (or adds if it did not exist before) the "
       "variable named NAME, stored in the database, and sets it to the "
       "given value in VALUE.  The variable is placed in the domain DOMAIN."),
    options::opts::none)
{
  if (args.size() != 3)
    throw usage(execid);

  var_domain d;
  var_name n;
  var_value v;
  internalize_var_domain(idx(args, 0), d);
  n = var_name(idx(args, 1)());
  v = var_value(idx(args, 2)());

  database db(app);
  db.set_var(make_pair(d, n), v);
}

CMD(unset, "unset", "", CMD_REF(variables), N_("DOMAIN NAME"),
    N_("Unsets a database variable"),
    N_("This command removes the variable NAME from domain DOMAIN, which "
       "was previously stored in the database."),
    options::opts::none)
{
  if (args.size() != 2)
    throw usage(execid);

  var_domain d;
  var_name n;
  internalize_var_domain(idx(args, 0), d);
  n = var_name(idx(args, 1)());
  var_key k(d, n);

  database db(app);
  N(db.var_exists(k), 
    F("no var with name %s in domain %s") % n % d);
  db.clear_var(k);
}

CMD(complete, "complete", "", CMD_REF(informative),
    N_("(revision|file|key) PARTIAL-ID"),
    N_("Completes a partial identifier"),
    "",
    options::opts::verbose)
{
  if (args.size() != 2)
    throw usage(execid);

  database db(app);
  project_t project(db);

  bool verbose = app.opts.verbose;

  N(idx(args, 1)().find_first_not_of("abcdef0123456789") == string::npos,
    F("non-hex digits in partial id"));

  if (idx(args, 0)() == "revision")
    {
      set<revision_id> completions;
      db.complete(idx(args, 1)(), completions);
      for (set<revision_id>::const_iterator i = completions.begin();
           i != completions.end(); ++i)
        {
          if (!verbose) cout << *i << '\n';
          else cout << describe_revision(project, *i) << '\n';
        }
    }
  else if (idx(args, 0)() == "file")
    {
      set<file_id> completions;
      db.complete(idx(args, 1)(), completions);
      for (set<file_id>::const_iterator i = completions.begin();
           i != completions.end(); ++i)
        cout << *i << '\n';
    }
  else if (idx(args, 0)() == "key")
    {
      typedef set< pair<key_id, utf8 > > completions_t;
      completions_t completions;
      db.complete(idx(args, 1)(), completions);
      for (completions_t::const_iterator i = completions.begin();
           i != completions.end(); ++i)
        {
          cout << i->first;
          if (verbose) cout << ' ' << i->second();
          cout << '\n';
        }
    }
  else
    throw usage(execid);
}

CMD_HIDDEN(test_migration_step, "test_migration_step", "", CMD_REF(db),
           "SCHEMA",
           N_("Runs one step of migration on the specified database"),
           N_("This command migrates the given database from the specified "
              "schema in SCHEMA to its successor."),
           options::opts::none)
{
  database db(app);
  key_store keys(app);

  if (args.size() != 1)
    throw usage(execid);
  db.test_migration_step(keys, idx(args,0)());
}

CMD_HIDDEN(rev_height, "rev_height", "", CMD_REF(informative), N_("REV"),
           N_("Shows a revision's height"),
           "",
           options::opts::none)
{
  if (args.size() != 1)
    throw usage(execid);
  revision_id rid(idx(args, 0)());
  database db(app);
  N(db.revision_exists(rid), F("no such revision '%s'") % rid);
  rev_height height;
  db.get_rev_height(rid, height);
  P(F("cached height: %s") % height);
}

// Local Variables:
// mode: C++
// fill-column: 76
// c-file-style: "gnu"
// indent-tabs-mode: nil
// End:
// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:
