/*
 * Copyright (c) 2002, The EROS Group, LLC and Johns Hopkins
 * University. All rights reserved.
 * 
 * This software was developed to support the EROS secure operating
 * system project (http://www.eros-os.org). The latest version of
 * the OpenCM software can be found at http://www.opencm.org.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 * 
 * 3. Neither the name of the The EROS Group, LLC nor the name of
 *    Johns Hopkins University, nor the names of its contributors
 *    may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/* OPENCM user agent command processing */

#include <opencm.h>
#include <repos/Repository.h>
#include "opencmclient.h"
#include <ctype.h>

static void
cmd_help(command *, int argc, char **argv);

static void
cmd_show_help(command *, int argc, char **argv);

/* Commands in a given group must be sorted alphabetically or the
 * minimal completion logic won't work.
 */

struct command options_cmds[] = {
  /* Note that these use a sleazy trick -- we are explicitly invoking
     the help function by using the cmd_show_help function as the
     applicable command. The end result is that either of

       opencm options basic
       opencm help options basic

     will yield documentation on options. */

  { "basic",      (void*)cmd_show_help,   0, 0, 0, "opt-basic.help" },
  { "debugging",  (void*)opencm_debugging_help,   0, CF_NOCONNECT, 0, "opt-debugging.help" },
  { "obscure",    (void*)cmd_show_help,   0, 0, 0, "opt-obscure.help" },
  { "server",     (void*)cmd_show_help,   0, 0, 0, "opt-server.help" },
  { (char *)0 }
} ;

struct command xdcs_cmds[] = {
  { "extract",     opencm_xdcs_extract,    3, CF_NOCONNECT, 0,
    (char *) 0 },
  { "insert",      opencm_xdcs_insert,     3, CF_NOCONNECT, 0,
    (char *) 0 },
  { "ls",          opencm_xdcs_ls,         1, CF_OPTARGS|CF_NOCONNECT, 0,
    (char *) 0 },
  { (char *)0 }
};

extern void opencm_xdcs_insert(WorkSpace *, int argc, char **argv);
extern void opencm_xdcs_extract(WorkSpace *, int argc, char **argv);

#ifdef REPLICATION
/* FIX: These are stale! */
struct command clone_cmds[] = {
  { "branch",      opencm_clone_branch,     2, 0, 0,
    "clone-branch.help" },
  { "project",     opencm_clone_project,    2, 0, 0,
    "clone-project.help" },
  { (char *)0 }
};
#endif

struct command create_cmds[] = {
  { "branch",      opencm_create_branch,     2, CF_NAME, 0,
    "create-branch.help" },
  { "group",       opencm_create_group,      1, CF_NAME, 0,
    "create-group.help" },
  { "project",     opencm_create_project,    1, CF_NAME, 0,
    "create-project.help" },
  { "repository",  opencm_create_repository, 3, 
    CF_NOCONNECT|CF_ADMIN, 0, 
    "create-repository.help" },
  { "user",        opencm_create_user,       1, 
    CF_NOCONNECT, 0,
    "create-user.help" },
  { (char *)0 }
};

struct command set_workspace_cmds[] = {
  { "repository",    opencm_set_workspace_repos,   
    1, CF_NOCONNECT|CF_WSINIT, 0,
    "set-ws-repos.help" },
  { "user",          opencm_set_workspace_user,    
    1,CF_NOCONNECT|CF_WSINIT, 0,
    "set-ws-user.help" },
  { (char *)0 }
};

struct command set_cmds[] = {
  { "description",   opencm_set_desc,    1, 0, 0,
    "set-description.help" },
  { "group",         opencm_set_group,   3, 0, 0,
    "set-group.help" },
  { "name",          opencm_set_name,    1, CF_NAME, 0,
    "set-name.help" },
  { "user",          opencm_set_user,    2, CF_ADMIN, 0,
    "set-user.help" },
  { "userkey",       opencm_set_userkey, 2, CF_ADMIN, 0,
    "set-userkey.help" },
  { "workspace",     0,                  0, 0, set_workspace_cmds, 0 },
  { (char *)0 }
};

struct command show_workspace_cmds[] = {
  { "mutable",    opencm_show_workspace_mutable,
    0, CF_NOCONNECT|CF_WSINIT, 0,
    "show-ws-mutable.help" },
  { "repository",    opencm_show_workspace_repos,
    0, CF_NOCONNECT|CF_WSINIT, 0,
    "show-ws-repos.help" },
  { "user",          opencm_show_workspace_user,
    0,CF_NOCONNECT|CF_WSINIT, 0,
    "show-ws-user.help" },
  { (char *)0 }
};

struct command show_cmds[] = {
  { "entity",        opencm_show_entity, 1, CF_OPTARGS, 0, 0 },
  { "object",        opencm_show_object, 1, CF_OPTARGS, 0, 0 },
  { "workspace",     0,                  0, 0, show_workspace_cmds, 0 },
  { (char *)0 }
};

struct command debug_cmds[] = {
  { "dump",      opencm_dump,       0, CF_WSINIT|CF_NOCONNECT, 0, 0 },
  { "mark",      opencm_mark,       0, CF_OPTARGS|CF_ADMIN, 0, 0 },
#ifdef DEBUG
  { "preds",     opencm_preds,      1, CF_OPTARGS|CF_WSINIT, 0, 0 },
  { "resolve",   opencm_resolve,    1, CF_OPTARGS, 0, 0 },
  { "vcmp",      opencm_vcmp,       2, CF_NOROOTSEARCH|CF_NOCONNECT, 0,
    0 },
#endif
  { "wsenumerate", opencm_wsenumerate,  0, CF_OPTARGS, 0, 0 },
  { "xdcs",      0,                 0, 0, xdcs_cmds, 0 },
  { (char *)0 }
};

struct command admin_enumerate_cmds[] = {
  { "entities",    opencm_enum_entities,  0, CF_ADMIN, 0, 0 },
  { "mutables",    opencm_enum_mutables,  0, CF_ADMIN|CF_OPTARGS, 0, 0 },
  { "users",       opencm_enum_users,     0, CF_ADMIN|CF_OPTARGS, 0, 0 },
  { (char *)0 }
};

struct command admin_cmds[] = {
  { "adduser",     opencm_add_user,   2, CF_ADMIN, 0,
    "adduser.help" },
  { "enumerate",   0,                 0, 0, admin_enumerate_cmds, 0 },
  { "gc",          opencm_gc,         0, CF_ADMIN, 0,
    "gc.help"      },
  { "upgrade",     opencm_upgrade,    0, CF_ADMIN|CF_NOCONNECT, 0,
    "upgrade.help" },
  { (char *)0 }
};

struct command top_cmds[] = {
  /*   { "repos", 0, 0, repos_cmd }, */
  { "add",       opencm_add_file,   1, CF_OPTARGS|CF_NOCONNECT|CF_WSINIT, 0,
    "add.help" },
  { "adduser",   opencm_add_user,   2, CF_ADMIN, 0,
    "adduser.help" },
  { "admin",     0,                 0, 0, admin_cmds, 0 },
  { "bind",      opencm_bind,       2, 0, 0,
    "bind.help" },
  { "browse",    opencm_browse,     0, CF_OPTARGS, 0, 0 },
  { "checkout",  opencm_checkout,   1, CF_WSINIT|CF_NOROOTSEARCH, 0,
    "checkout.help" },
#ifdef REPLICATION
  { "clone",     0,                 0, 0, clone_cmds, 0 },
#endif
  { "commit",    opencm_commit,     0, CF_OPTARGS|CF_WSINIT, 0,
    "commit.help" },
  { "create",    0,                 0, 0, create_cmds, 0 },
  /* eventually CS_MORE */
  { "debug",     0,                 0, 0, debug_cmds, 0 },
  { "diff",      opencm_diff,       0, CF_OPTARGS|CF_WSINIT, 0,
    "diff.help" },
  { "gadd",      opencm_add_member,     2, 0, 0,
    "gadd.help" },
  { "gremove",   opencm_remove_member,  2, 0, 0,
    "gremove.help" },
  { "help",      (void*)cmd_help,   0, 0, 0, 0 },
  { "log",       opencm_log,        0, CF_OPTARGS|CF_WSINIT, 0, 
    "log.help" },
  { "import",    opencm_import,     1, CF_NAME|CF_WSINIT|CF_NOROOTSEARCH, 0, 
    "import.help" },
  { "logmail",   opencm_logmail,    1, CF_NOROOTSEARCH, 0,
    "logmail.help" },
  { "ls",        opencm_ls,         0, CF_OPTARGS, 0,
    "ls.help" },
  { "merge",     opencm_merge,      1, CF_WSINIT, 0,
    "merge.help" },
  { "mkdir",     opencm_mkdir,      1, 0, 0,
    "mkdir.help" },
  { "mv",        opencm_rename,      2, CF_WSINIT|CF_NOCONNECT, 0,
    "mv.help" },

  /* NOTE: This is for testing the new diff algorithm. It will later
     be removed. */
  { "ndiff",     opencm_ndiff,      2, CF_NOCONNECT, 0 },

  { "note",      opencm_note,       0, CF_WSINIT|CF_NOCONNECT, 0,
    "note.help" },
  { "notes",     opencm_show_notes, 0, CF_WSINIT|CF_NOCONNECT, 0,
    "notes.help" },
  { "options",   0,                 0, 0, options_cmds, 0 },
  { "rebind",    opencm_rebind,     2, 0, 0,
    "rebind.help" }, 
  { "revert",    opencm_revert,     0, CF_WSINIT, 0,
    "revert.help" },
  { "rm",        opencm_remove_file,1, CF_OPTARGS|CF_NOCONNECT|CF_WSINIT, 0,
    "rm.help" },
  { "server",    opencm_server,     1, CF_NOCONNECT, 0,
    "server.help" },
  { "set",       0,                 0, 0, set_cmds, 0 },
  { "show",      0,                 0, 0, show_cmds, 0 },
  { "status",    opencm_status,     0, CF_OPTARGS|CF_WSINIT|CF_NOCONNECT, 0,
    "status.help" },
  { "tag",       opencm_tag,        2, CF_NAME, 0,
    "tag.help" },
  { "unbind",    opencm_unbind,     1, 0, 0,
    "unbind.help" }, 
  /* eventually CS_MORE: */
  { "update",    opencm_update,     0, CF_WSINIT, 0,
    "update.help" },
  { "version",   opencm_version,    0, CF_OPTCONNECT, 0,
    "version.help" },
  { "whoami",    opencm_whoami,     0, CF_NOCONNECT|CF_WSINIT, 0,
    "whoami.help" },
  { (char *)0 }
};

/*
 * Results of command search.
 */
#define	CMD_UNIQUE	0
#define	CMD_FOUND	1
#define	CMD_NONE	2
#define	CMD_AMBIGUOUS	3
#define	CMD_INCOMPLETE	4

static int
cmd_search(char *name, command *table, command **cmdp)
{
  struct command	*cmd;
  int			result = CMD_NONE;

  if (name == 0)
    return CMD_INCOMPLETE;
  
  for (cmd = table; cmd->name != 0; cmd++) {
    register char *lp;
    register char *rp;
    register int  c;

    lp = name;
    rp = cmd->name;
    while ((c = *lp) == *rp) {
      if (c == 0) {
	/* complete match */
	*cmdp = cmd;
	return (CMD_UNIQUE);
      }
      lp++;
      rp++;
    }
    if (c == 0) {
      /* end of name, not end of command -
	 partial match */
      if (result == CMD_FOUND) {
	result = CMD_AMBIGUOUS;
	/* but keep looking for a full match -
	   this lets us match single letters */
      }
      else {
	*cmdp = cmd;
	result = CMD_FOUND;
      }
    }
  }

#if 0
  /* It is okay to leave this alone -- it doesn't override the main
  help command, and it serves to signal that the user really doesn't
  want to run a command. On the other hand, it has a way of appearing
  in the first argument position if you are not careful. Better to
  require it at the front. */
  if (result == CMD_NONE) {
    /* check for 'help' */
    if (name[0] == 'h' && name[1] == 'e'
	&& name[2] == 'l' && name[3] == 'p')
      result = CMD_INCOMPLETE;
  }
#endif

  return (result);
}

static void
cmd_help(command *cmd, int argc, char **argv)
{
  /* Dummy placeholder function so we can run a proper help
     command. */
}

static void
cmd_show_help(command *cmd, int argc, char **argv)
{
  /* Dummy placeholder function so we can implement commands whose
     sole purpose is to display a help file. */
}

/* Trim leading and trailing whitespace (in place) */
static void
trim_ws(char *instr)
{
  char *nullTerm = instr;
  if (instr) {
    char *x = instr;
    char *cp = instr;
    while(*x) {
      /* skip leading spaces */
      while(*x && isspace(*x))
	x++;

      /* just in case there are embedded spaces */
      if (*x && (cp != instr))
	*(cp++) = ' ';

      /* shift chars left */
      while(*x && !isspace(*x)) {
	*cp = *x;
	cp++;
	x++;
      }
      nullTerm = cp;
    }
    /* Set the terminating null */
    *nullTerm = '\0';
  }
}

/* Ensure only legal characters in input string */
static void
validate_optName(const char *instr)
{
  if (instr == NULL || strlen(instr) == 0)
    THROW(ExBadValue, "Must provide option input.");

#define OPTNAME_MAX 40
  if (strlen(instr) > OPTNAME_MAX)
    THROW(ExBadValue, format("Input is longer than %u chars.", OPTNAME_MAX));
#undef OPTNAME_MAX

  while(*instr) {
    char c = *instr++;
    if (!isalnum(c) && c != ' ' && c != '_' &&
	c != '-' && c != '@' && c != '.')
      THROW(ExBadValue, format("Illegal char in input: %c", c));
  }
}

void
list_commands(command *table)
{
  int i = 0;
  command *cmd;

  xprintf("The " CM_APPNAME " commands are:\n\n");

  for (cmd = table; cmd->name != 0; cmd++) {
    xprintf("%-12s", cmd->name);
    i++;
    if ((i % 6) == 0)
      xprintf("\n");
  }

  if (i % 6)
    xprintf("\n");
}

struct found_command
opencm_find_command(int argc, char **argv)
{
  command *cmd_table = top_cmds;
  command *cmd = top_cmds;
  struct found_command fc;
  unsigned nargs;
  int wantHelp = 0;

  fc.cmd = 0;
  fc.argc = 0;
  fc.argv = 0;  

  while (cmd_table) {
    int result = cmd_search(argv[0], cmd_table, &cmd);

    switch (result) {
    case CMD_NONE:
      THROW(ExBadValue, "No such command.");
      
    case CMD_AMBIGUOUS:
      THROW(ExBadValue, "Ambiguous command.");
      
    case CMD_INCOMPLETE:
      list_commands(cmd_table);
      return fc;
      
    default:
      /* If this was the help command, continue parsing out until
	 there is resolution. */

      if (cmd->fcn == (void *)cmd_help) {
	wantHelp = 1;
	/* Note that this break does NOT update cmd_table, which
	   allows the user to type something like

	      opencm help create

            in order to get help on the create subcommand. That is:
	   the help subcommand is position-neutral. */

	break;
      }
      else if (cmd->fcn == (void*)cmd_show_help) {
	wantHelp = 1;
	goto done;
      }

      cmd_table = cmd->more;

      break;
    }      

    --argc;
    ++argv;
  }
 done:

  if (cmd == 0)
    THROW(ExBadValue, "No such command.");

  nargs = cmd->nargs;
  
  if (wantHelp) {
    if (cmd->docFile)
      ShowHelp(cmd->docFile);
    else if (cmd_table)
      list_commands(cmd_table);
    else
      log_trace(TRC_HELP, "No help is available on incomplete commands.\n");
    return fc;
  }

  if(((argc < nargs) || (argc > nargs && !CMD_ISFLAG(cmd, CF_OPTARGS)))
      && cmd->docFile)
  {
    ShowHelp(cmd->docFile);
    return fc;
  }

  if ( argc < nargs )
    THROW(ExBadValue, 
	  format("Argument mismatch. Command requires %u "
		 "arguments.", nargs));
  else if ( argc > nargs && !CMD_ISFLAG(cmd, CF_OPTARGS))
    THROW(ExBadValue, 
	  format("Argument mismatch. Command takes at most %u "
		 "arguments.", nargs));

  /* If we need an opt_Name for this command, get it and
   * make sure it's valid */
  if (CMD_ISFLAG(cmd, CF_NAME)) {
    while (opt_Name == 0)
      opt_Name = opencm_readline("Synopsis", 0);

    trim_ws(opt_Name);
    validate_optName(opt_Name);
  }

  fc.cmd = cmd;
  fc.argc = argc;;
  fc.argv = argv;

  return fc;
}
