pennmush/game/data/
pennmush/game/log/
pennmush/game/save/
pennmush/game/txt/evt/
pennmush/game/txt/nws/
pennmush/os2/
pennmush/po/
pennmush/win32/msvc.net/
pennmush/win32/msvc6/
/**
 * \file help.c
 *
 * \brief The PennMUSH help system.
 *
 *
 */
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include "conf.h"
#include "externs.h"
#include "command.h"
#include "htab.h"
#include "help.h"
#include "log.h"
#include "ansi.h"
#include "parse.h"
#include "pueblo.h"
#include "flags.h"
#include "dbdefs.h"
#include "mymalloc.h"
#include "confmagic.h"

HASHTAB help_files;  /**< Help filenames hash table */

static int help_init = 0;

static void do_new_spitfile(dbref player, char *arg1, help_file *help_dat);
static const char *string_spitfile(help_file *help_dat, char *arg1);
static help_indx *help_find_entry(help_file *help_dat, const char *the_topic);
static char *list_matching_entries(const char *pattern, help_file *help_dat);

static void help_build_index(help_file *h, int restricted);

/** Linked list of help topic names. */
typedef struct TLIST {
  char topic[TOPIC_NAME_LEN + 1];	/**< Name of topic */
  struct TLIST *next;			/**< Pointer to next list entry */
} tlist;

tlist *top = NULL;   /**< Pointer to top of linked list of topic names */

help_indx *topics = NULL;  /**< Pointer to linked list of topic indexes */
unsigned num_topics = 0;   /**< Number of topics loaded */
unsigned top_topics = 0;   /**< Maximum number of topics loaded */

static void write_topic(long int p);

#define TRUE 1	 /**< A true value */
#define FALSE 0	 /**< A false value */

COMMAND (cmd_helpcmd) {
  help_file *h;

  h = hashfind(cmd->name, &help_files);

  if (!h) {
    notify(player, T("That command is unavailable."));
    return;
  }

  if (h->admin && !Hasprivs(player)) {
    notify(player, T("You don't look like an admin to me."));
    return;
  }

  if (wildcard(arg_left))
    notify_format(player, T("Here are the entries which match '%s':\n%s"),
		  arg_left, list_matching_entries(arg_left, h));
  else
    do_new_spitfile(player, arg_left, h);
}

/** Initialize the helpfile hashtable, which contains the names of the
 * help files.
 */
void
init_help_files(void)
{
  hash_init(&help_files, 8, sizeof(help_file));
  help_init = 1;
}

/** Add new help command. This function is
 * the basis for the help_command directive in mush.cnf. It creates
 * a new help entry for the hash table, builds a help index,
 * and adds the new command to the command table.
 * \param command_name name of help command to add.
 * \param filename name of the help file to use for this command.
 * \param admin if 1, this command reads admin topics, rather than standard.
 */
void
add_help_file(const char *command_name, const char *filename, int admin)
{
  help_file *h;
  char newfilename[256] = "\0";

  /* Must use a buffer for MacOS file path conversion */
  strncpy(newfilename, filename, 256);

  if (help_init == 0)
    init_help_files();

  if (!command_name || !filename || !*command_name || !*newfilename)
    return;

  /* If there's already an entry for it, complain */
  h = hashfind(strupper(command_name), &help_files);
  if (h) {
    do_rawlog(LT_ERR, T("Duplicate help_command %s ignored."), command_name);
    return;
  }

  h = mush_malloc(sizeof *h, "help_file.entry");
  h->command = mush_strdup(strupper(command_name), "help_file.command");
  h->file = mush_strdup(newfilename, "help_file.filename");
  h->entries = 0;
  h->indx = NULL;
  h->admin = admin;
  help_build_index(h, h->admin);
  if (!h->indx) {
    mush_free(h->command, "help_file.command");
    mush_free(h->file, "help_file.filename");
    mush_free(h, "help_file.entry");
    return;
  }
  (void) command_add(h->command, CMD_T_ANY | CMD_T_NOPARSE, NULL, 0, NULL,
		     cmd_helpcmd);
  hashadd(h->command, h, &help_files);
}

/** Rebuild a help file index.
 * \verbatim
 * This command implements @readcache.
 * \endverbatim
 * \param player the enactor.
 */
void
help_reindex(dbref player)
{
  help_file *curr;

  for (curr = (help_file *) hash_firstentry(&help_files);
       curr; curr = (help_file *) hash_nextentry(&help_files)) {
    if (curr->indx) {
      mush_free((Malloc_t) curr->indx, "help_index");
      curr->entries = 0;
    }
    help_build_index(curr, curr->admin);
  }
  if (player != NOTHING) {
    notify(player, T("Help files reindexed."));
    do_rawlog(LT_WIZ, T("Help files reindexed by %s(#%d)"), Name(player),
	      player);
  } else
    do_rawlog(LT_WIZ, T("Help files reindexed."));
}

static void
do_new_spitfile(dbref player, char *arg1, help_file *help_dat)
{
  help_indx *entry = NULL;
  FILE *fp;
  char *p, line[LINE_SIZE + 1];
  char the_topic[LINE_SIZE + 2];
  int default_topic = 0;
  size_t n;

  if (*arg1 == '\0') {
    default_topic = 1;
    arg1 = (char *) help_dat->command;
  } else if (*arg1 == '&') {
    notify(player, T("Help topics don't start with '&'."));
    return;
  }
  if (strlen(arg1) > LINE_SIZE)
    *(arg1 + LINE_SIZE) = '\0';

  if (help_dat->admin) {
    sprintf(the_topic, "&%s", arg1);
  } else
    strcpy(the_topic, arg1);

  if (!help_dat->indx || help_dat->entries == 0) {
    notify(player, T("Sorry, that command is temporarily unvailable."));
    do_rawlog(LT_ERR, T("No index for %s."), help_dat->command);
    return;
  }

  entry = help_find_entry(help_dat, the_topic);
  if (!entry && default_topic)
    entry = help_find_entry(help_dat, (help_dat->admin ? "&help" : "help"));

  if (!entry) {
    notify_format(player, T("No entry for '%s'."), arg1);
    return;
  }

  if ((fp = fopen(help_dat->file, FOPEN_READ)) == NULL) {
    notify(player, T("Sorry, that function is temporarily unavailable."));
    do_log(LT_ERR, 0, 0, T("Can't open text file %s for reading"),
	   help_dat->file);
    return;
  }
  if (fseek(fp, entry->pos, 0) < 0L) {
    notify(player, T("Sorry, that function is temporarily unavailable."));
    do_rawlog(LT_ERR, T("Seek error in file %s"), help_dat->file);
    return;
  }
  strcpy(the_topic, strupper(entry->topic + (*entry->topic == '&')));
  /* ANSI topics */
  if (ShowAnsi(player)) {
    char ansi_topic[LINE_SIZE + 10];
    sprintf(ansi_topic, "%s%s%s", ANSI_HILITE, the_topic, ANSI_NORMAL);
    notify(player, ansi_topic);
  } else
    notify(player, the_topic);

  if (SUPPORT_PUEBLO)
    notify_noenter(player, tprintf("%cSAMP%c", TAG_START, TAG_END));
  for (n = 0; n < BUFFER_LEN; n++) {
    if (fgets(line, LINE_SIZE, fp) == NULL)
      break;
    if (line[0] == '&')
      break;
    if (line[0] == '\n') {
      notify(player, " ");
    } else {
      for (p = line; *p != '\0'; p++)
	if (*p == '\n')
	  *p = '\0';
      notify(player, line);
    }
  }
  if (SUPPORT_PUEBLO)
    notify_format(player, "%c/SAMP%c", TAG_START, TAG_END);
  fclose(fp);
  if (n >= BUFFER_LEN)
    notify_format(player, T("%s output truncated."), help_dat->command);
}


static help_indx *
help_find_entry(help_file *help_dat, const char *the_topic)
{
  size_t n;
  help_indx *entry = NULL;

  if (help_dat->entries < 10) {	/* Just do a linear search for small files */
    for (n = 0; n < help_dat->entries; n++) {
      if (string_prefix(help_dat->indx[n].topic, the_topic)) {
	entry = &help_dat->indx[n];
	break;
      }
    }
  } else {			/* Binary search of the index */
    int left = 0;
    int cmp;
    int right = help_dat->entries - 1;

    while (1) {
      n = (left + right) / 2;

      if (left > right)
	break;

      cmp = strcasecmp(the_topic, help_dat->indx[n].topic);

      if (cmp == 0) {
	entry = &help_dat->indx[n];
	break;
      } else if (cmp < 0) {
	/* We need to catch the first prefix */
	if (string_prefix(help_dat->indx[n].topic, the_topic)) {
	  int m;
	  for (m = n - 1; m >= 0; m--) {
	    if (!string_prefix(help_dat->indx[m].topic, the_topic))
	      break;
	  }
	  entry = &help_dat->indx[m + 1];
	  break;
	}
	if (left == right)
	  break;
	right = n - 1;
      } else {			/* cmp > 0 */
	if (left == right)
	  break;
	left = n + 1;
      }
    }
  }
  return entry;
}

static void
write_topic(long int p)
{
  tlist *cur, *nextptr;
  help_indx *temp;
  for (cur = top; cur; cur = nextptr) {
    nextptr = cur->next;
    if (num_topics >= top_topics) {
      top_topics += top_topics / 2 + 20;
      if (topics)
	topics = (help_indx *) realloc(topics, top_topics * sizeof(help_indx));
      else
	topics = (help_indx *) malloc(top_topics * sizeof(help_indx));
      if (!topics) {
	mush_panic(T("Out of memory"));
      }
    }
    temp = &topics[num_topics++];
    temp->pos = p;
    strcpy(temp->topic, cur->topic);
    free(cur);
  }
  top = NULL;
}

static int WIN32_CDECL topic_cmp(const void *s1, const void *s2);
static int WIN32_CDECL
topic_cmp(const void *s1, const void *s2)
{
  const help_indx *a = s1;
  const help_indx *b = s2;

  return strcasecmp(a->topic, b->topic);

}

static void
help_build_index(help_file *h, int restricted)
{
  long bigpos, pos = 0;
  int in_topic;
  int i, n, lineno, ntopics;
  char *s, *topic;
  char the_topic[TOPIC_NAME_LEN + 1];
  char line[LINE_SIZE + 1];
  FILE *rfp;
  tlist *cur;

  /* Quietly ignore null values for the file */
  if (!h || !h->file)
    return;
  if ((rfp = fopen(h->file, FOPEN_READ)) == NULL) {
    do_rawlog(LT_ERR, T("Can't open %s for reading"), h->file);
    return;
  }

  if (restricted)
    do_rawlog(LT_WIZ, T("Indexing file %s (admin topics)"), h->file);
  else
    do_rawlog(LT_WIZ, T("Indexing file %s"), h->file);
  topics = NULL;
  num_topics = 0;
  top_topics = 0;
  bigpos = 0L;
  lineno = 0;
  ntopics = 0;

  in_topic = 0;

  while (fgets(line, LINE_SIZE, rfp) != NULL) {
    ++lineno;
    if (ntopics == 0) {
      /* Looking for the first topic, but we'll ignore blank lines */
      if (!line[0]) {
	/* Someone's feeding us /dev/null? */
	do_rawlog(LT_ERR, T("Malformed help file %s doesn't start with &"),
		  h->file);
	fclose(rfp);
	return;
      }
      if (isspace((unsigned char) line[0]))
	continue;
      if (line[0] != '&') {
	do_rawlog(LT_ERR, T("Malformed help file %s doesn't start with &"),
		  h->file);
	fclose(rfp);
	return;
      }
    }
    n = strlen(line);
    if (line[n - 1] != '\n') {
      do_rawlog(LT_ERR, T("Line %d of %s: line too long"), lineno, h->file);
    }
    if (line[0] == '&') {
      ++ntopics;
      if (!in_topic) {
	/* Finish up last entry */
	if (ntopics > 1) {
	  write_topic(pos);
	}
	in_topic = TRUE;
      }
      /* parse out the topic */
      /* Get the beginning of the topic string */
      for (topic = &line[1];
	   (*topic == ' ' || *topic == '\t') && *topic != '\0'; topic++) ;

      /* Get the topic */
      strcpy(the_topic, "");
      for (i = -1, s = topic; *s != '\n' && *s != '\0'; s++) {
	if (i >= TOPIC_NAME_LEN - 1)
	  break;
	if (*s != ' ' || the_topic[i] != ' ')
	  the_topic[++i] = *s;
      }
      if ((restricted && the_topic[0] == '&')
	  || (!restricted && the_topic[0] != '&')) {
	the_topic[++i] = '\0';
	cur = (tlist *) malloc(sizeof(tlist));
	strcpy(cur->topic, the_topic);
	cur->next = top;
	top = cur;
      }
    } else {
      if (in_topic) {
	pos = bigpos;
      }
      in_topic = FALSE;
    }
    bigpos = ftell(rfp);
  }

  /* Handle last topic */
  write_topic(pos);
  qsort(topics, num_topics, sizeof(help_indx), topic_cmp);
  h->entries = num_topics;
  h->indx = topics;
  add_check("help_index");
  fclose(rfp);
  do_rawlog(LT_WIZ, T("%d topics indexed."), num_topics);
  return;
}

/* ARGSUSED */
FUNCTION(fun_textfile)
{
  help_file *h;

  h = hashfind(strupper(args[0]), &help_files);
  if (!h) {
    safe_str(T("#-1 NO SUCH FILE"), buff, bp);
    return;
  }
  if (h->admin && !Hasprivs(executor)) {
    safe_str(T(e_perm), buff, bp);
    return;
  }

  if (wildcard(args[1]))
    safe_str(list_matching_entries(args[1], h), buff, bp);
  else
    safe_str(string_spitfile(h, args[1]), buff, bp);
}


static const char *
string_spitfile(help_file *help_dat, char *arg1)
{
  help_indx *entry = NULL;
  FILE *fp;
  char line[LINE_SIZE + 1];
  char the_topic[LINE_SIZE + 2];
  size_t n;
  static char buff[BUFFER_LEN];
  char *bp;

  if (*arg1 == '\0')
    arg1 = (char *) "help";
  else if (*arg1 == '&')
    return T("#-1 INVALID ENTRY");
  if (strlen(arg1) > LINE_SIZE)
    *(arg1 + LINE_SIZE) = '\0';

  if (help_dat->admin)
    sprintf(the_topic, "&%s", arg1);
  else
    strcpy(the_topic, arg1);

  if (!help_dat->indx || help_dat->entries == 0)
    return T("#-1 NO INDEX FOR FILE");

  entry = help_find_entry(help_dat, the_topic);

  if (!entry) {
    return T("#-1 NO ENTRY");
  }

  if ((fp = fopen(help_dat->file, FOPEN_READ)) == NULL) {
    return T("#-1 UNAVAILABLE");
  }
  if (fseek(fp, entry->pos, 0) < 0L) {
    return T("#-1 UNAVAILABLE");
  }
  bp = buff;
  for (n = 0; n < BUFFER_LEN; n++) {
    if (fgets(line, LINE_SIZE, fp) == NULL)
      break;
    if (line[0] == '&')
      break;
    safe_str(line, buff, &bp);
  }
  *bp = '\0';
  fclose(fp);
  return buff;
}

/** Return a string with all help entries that match a pattern */
static char *
list_matching_entries(const char *pattern, help_file *help_dat)
{
  static char buff[BUFFER_LEN];
  int offset;
  char *bp;
  size_t n;

  bp = buff;

  if (help_dat->admin)
    offset = 1;			/* To skip the leading & */
  else
    offset = 0;

  for (n = 0; n < help_dat->entries; n++)
    if (quick_wild(pattern, help_dat->indx[n].topic + offset)) {
      safe_str(help_dat->indx[n].topic + offset, buff, &bp);
      safe_strl(", ", 2, buff, &bp);
    }

  if (bp > buff)
    *(bp - 2) = '\0';
  else {
    safe_str(T("No matching help topics."), buff, &bp);
    *bp = '\0';
  }

  return buff;
}