#include <kernel/kernel.h>

#include <phantasmal/phrase.h>
#include <phantasmal/log.h>
#include <phantasmal/lpc_names.h>

#include <limits.h>
#include <type.h>
#include <trace.h>
#include <status.h>

inherit COMMON_AUTO;

/* Reference to Soundexd */
private object  soundex;

/* UNQ DTD for help entries */
private object  help_dtd;

/* Number of locales */
private int     num_loc;

/* Help entries */
private mixed*  exact_entries;
private mapping sdx_entries;

/* Known help directories/files */
private mixed*  path_stack;

/* Paths to exclude */
private mapping path_exc;

/* Vars for loading in call_outs */
string *files_to_load;
int     load_callout;

/* Prototypes */
void load_unq_help(string path, mixed* data);
void upgraded(varargs int clone);
void reread_help_files(void);
void clear_help_entries(void);
void new_help_file(string path);
void new_help_directory(string path);
static void load_helpfiles(void);


#define FILES_PER_ITER 1


static void create(varargs int clone) {
  if(clone)
    error("Can't clone helpd!");

  if(!find_object(UNQ_PARSER))
    compile_object(UNQ_PARSER);
  if(!find_object(UNQ_DTD))
    compile_object(UNQ_DTD);

  if(!find_object(SOUNDEXD)) { compile_object(SOUNDEXD); }
  soundex = find_object(SOUNDEXD);

  path_stack = ({ });

  files_to_load = ({ });
  load_callout = -1;

  clear_help_entries();
}

void destructed(int clone) {
  if(SYSTEM() && help_dtd)
    destruct_object(help_dtd);
}

void upgraded(varargs int clone) {
  if(SYSTEM()) {
    files_to_load = ({ });
    load_callout = -1;

    clear_help_entries();
    rlimits(status()[ST_STACKDEPTH];-1) {
      reread_help_files();
    }
  }
}


/* This function takes a list of strings like that returned by explode()
   and calls explode on the individual members of it.  Used to explode
   wordlist around more than one delimiter. */
private string* reexplode_wordlist(string *wordlist, string delim) {
  string *newwords;
  int     ctr;

  newwords = ({ });
  for(ctr = 0; ctr < sizeof(wordlist); ctr++) {
    if(wordlist[ctr] && strlen(wordlist[ctr])) {
      newwords += explode(wordlist[ctr], delim);
    }
  }

  return newwords;
}

private string normalize_help_query(string query) {
  int     ctr, ctr2;
  string *words, *querylist;
  string  word;

  query = STRINGD->to_lower(STRINGD->trim_whitespace(query));

  words = explode(query, " ");

  /* Re-explode the list around "-" and "_", just like space */
  words = reexplode_wordlist(words, "-");
  words = reexplode_wordlist(words, "_");

  querylist = ({ });

  for(ctr = 0; ctr < sizeof(words); ctr++) {
    if(words[ctr]) {
      word = "";
      for(ctr2 = 0; ctr2 < strlen(words[ctr]); ctr2++) {
	if((words[ctr][ctr2] >= "A"[0] && words[ctr][ctr2] <= "Z"[0])
	   || (words[ctr][ctr2] >= "a"[0] && words[ctr][ctr2] <= "z"[0])
	   || (words[ctr][ctr2] >= "0"[0] && words[ctr][ctr2] <= "9"[0])) {
	  word += words[ctr][ctr2..ctr2];
	}
      }

      if(strlen(word)) {
	querylist += ({ word });
      }
    }
  }

  /* Alphabetize the query list */
  querylist = STRINGD->alpha_sort_list(querylist);

  query = implode(querylist, " ");

  return query;
}

void clear_help_entries(void) {
  int iter;

  if(GAME() || COMMON() || SYSTEM()) {
    num_loc = PHRASED->num_locales();

    exact_entries = allocate(num_loc);
    for(iter = 0; iter < num_loc; iter++) {
      exact_entries[iter] = ([ ]);
    }
    sdx_entries = ([ ]);
  }
}

void reread_help_files(void) {
  int iter;
  int ret;

  if(GAME() || COMMON() || SYSTEM()) {

    for(iter = 0; iter < sizeof(path_stack); iter++) {
      new_help_directory(path_stack[iter]);
      if(iter > 10) {
	error("Error in iteration...\n");
      }
    }
  }
}

/* Customize this for your MUD... */
private int exclude_path(string path) {
  /* Don't include SCCS directories */
  if(sscanf(path, "%*s/SCCS%*s") == 2) {
    return 1;
  }
  /* Don't include RCS directories */
  if(sscanf(path, "%*s/RCS%*s") == 2) {
    return 1;
  }
  /* Don't include CVS directories */
  if(sscanf(path, "%*s/CVS%*s") == 2) {
    return 1;
  }

  return 0;
}

void load_help_dtd(string dtd) {
  if(SYSTEM()) {
    if(help_dtd)
      error("Help DTD already exists!");

    help_dtd = clone_object(UNQ_DTD);
    help_dtd->load(dtd);
  }
}

void new_help_file(string path) {
  string  contents, word, arg;
  string* lines;
  int     ctr, argcount, len;
  mixed*  keys;
  mixed*  ent;
  mixed*  exp_arr;
  mixed*  unq_data;
  string err;

  if(GAME() || COMMON() || SYSTEM()) {

    contents = read_file(path);
    if(strlen(contents) > MAX_STRING_SIZE - 3) {
      error("Helpfile " + path + " too long!");
    }

    err = catch (unq_data
		 = UNQ_PARSER->unq_parse_with_dtd(contents, help_dtd, path));

    if (err != nil) {
      LOGD->write_syslog("Helpd got parse error parsing " + path,
			 LOG_ERR);
      error(err);
    }
    if(!unq_data || !typeof(unq_data) == T_ARRAY)
      error("Couldn't load file " + path + " as UNQ helpfile!");

    if(unq_data && typeof(unq_data) == T_ARRAY && unq_data[0] &&
       unq_data[0] != "") {
      load_unq_help(path, unq_data);
      return;
    }

    error("Couldn't load UNQ file " + path);
  }
}

void new_help_directory(string path) {
  mixed** dir;
  int     ctr;
  string left;

  if(GAME() || COMMON() || SYSTEM()) {

    if(previous_program() != HELPD) {
      path_stack += ({ path });
    }

    dir = get_dir(path + "/*");
    left = "";
    for(ctr = 0; ctr < sizeof(dir[0]); ctr++) {
      if(dir[1][ctr] == -2) {
	if(!exclude_path(path + "/" + dir[0][ctr])) {
	  new_help_directory(path + "/" + dir[0][ctr]);
	}
      } else if(sscanf(dir[0][ctr], "%*s.hlp%s", left) == 2) {
	if(left == "") {
	  files_to_load += ({ path + "/" + dir[0][ctr] });

	  if(load_callout < 0) {
#if 0
	    load_callout = call_out("load_helpfiles", 0);
	    if(load_callout < 0)
	      LOGD->write_syslog("Couldn't load all helpfiles?", LOG_ERR);
#else
	    /* Temporary hack to keep from using the call_out loading */
	    load_helpfiles();
#endif
	  }
	}
      }
    }
  }
}


void clear_registered_help_dirs(void) {
  if(SYSTEM() || COMMON() || GAME()) {
    path_stack = ({ });
  }
}


/* Note: The mapping (tmp) handles uniqueness of description by
   hashing the descriptions (Phrase LWOs) and taking the map_values.
   The if/for combo actually checks for a match on keywords.  An entry
   matches if all of its keywords are also in kw.  If it has no
   keywords, it matches no matter what kw is. */
private mixed* filter_for_keywords(mixed* entries, mixed* kw) {
  mixed*  ret;
  int     ctr;
  mapping tmp;

  if(!entries) return nil;

  tmp = ([ ]);

  /* To make later checking easier */
  if(!kw) kw = ({ });

  for(ctr = 0; ctr < sizeof(entries); ctr++) {
    if(!tmp[entries[ctr][1]]) {
      /* Not a repeat of an existing entry */

      if(sizeof(kw & entries[ctr][4]) == sizeof(entries[ctr][4])) {
	/* If all keywords in the entry are also in kw, we'll
	   put it into tmp. */
	tmp[entries[ctr][1]] = entries[ctr];
      }
    }
  }

  ret = map_values(tmp);
  return sizeof(ret) ? ret : nil;
}

mixed* query_exact(string key, object user) {
  int locale;
  mixed* ent;

  if(GAME() || COMMON() || SYSTEM()) {

    locale = user->get_locale();

    key = normalize_help_query(key);

    ent = exact_entries[locale][key];
    if(!ent) {
      ent = exact_entries[LANG_englishUS][key];
    }

    return ent;
  } else
    return nil;
}

mixed* query_exact_with_keywords(string key, object user, string* kw) {
  if(GAME() || COMMON() || SYSTEM()) {
    return filter_for_keywords(query_exact(key, user), kw);
  } else
    return nil;
}

mixed* query_soundex(string sdx_key, object user) {
  int locale;
  locale = user->get_locale();

  if(GAME() || COMMON() || SYSTEM()) {

    if(locale != LANG_englishUS)
      return nil;

    return sdx_entries[sdx_key];
  } else
    return nil;
}

mixed* query_soundex_with_keywords(string sdx_key, object user, string* kw) {
  mixed* entries;

  if(GAME() || COMMON() || SYSTEM()) {
    entries = query_soundex(sdx_key, user);
    return filter_for_keywords(entries, kw);
  } else
    return nil;
}

private void new_unq_entry(string path, object names, object desc,
			   string keywords) {
  int    locale, len, ctr;
  mixed* keys, arr, kw_arr;
  string sdx;
  object phr;

  if(!GAME() && !COMMON() && !SYSTEM())
    return;

  if(!names || !desc) {
    error("Required help component(s) missing in file: " + path);
  }

  if(keywords && keywords != "" && !STRINGD->is_whitespace(keywords)) {
    kw_arr = explode(keywords, ",");
    for(ctr = 0; ctr < sizeof(kw_arr); ctr++) {
      kw_arr[ctr] = STRINGD->trim_whitespace(kw_arr[ctr]);

      if(kw_arr[ctr] != "admin") {
	LOGD->write_syslog("Unknown help keyword '" + kw_arr[ctr]
			   + "' loading helpfiles!", LOG_WARN);
      }
    }
  } else {
    kw_arr = ({ });
  }

  for(locale = 0; locale < num_loc; locale++) {
    string tmp;

    tmp = names->get_content_by_lang(locale);
    if(!tmp || tmp == "")
      continue;

    keys = explode(names->get_content_by_lang(locale), ",");
    len = sizeof(keys);

    for(ctr = 0; ctr < len; ctr++) {
      keys[ctr] = normalize_help_query(keys[ctr]);
      if(STRINGD->string_has_char('\n', keys[ctr])) {
	LOGD->write_syslog("Embedded newline in name(s) of help entry \""
				  + keys[0] + "\"", LOG_ERR);
      }

      /* If no entry, init array */
      if(!exact_entries[locale][keys[ctr]])
	exact_entries[locale][keys[ctr]] = ({ });

      if(locale == LANG_englishUS) {
	/* Generate Soundex key */
	sdx = soundex->get_key(keys[ctr]);
      } else {
	/* If it's not English, don't bother with Soundex */
	sdx = "";
      }

      /* Compose entry */
      arr = ({keys[ctr], desc, path, sdx, kw_arr });

      if(locale == LANG_englishUS) {
	if(!sdx_entries[sdx])
	  sdx_entries[sdx] = ({ });

	/* If it's English, put its Soundex into the sdx_entries array */
	sdx_entries[sdx] += ({ arr });
      }

      /* Put exact entry into specified locale */
      exact_entries[locale][keys[ctr]] += ({ arr });
    }
  }
}

void load_unq_help(string path, mixed* data) {
  int    len, ctr;
  string keywords;
  object names, desc;

  if(!GAME() && !COMMON() && !SYSTEM())
    return;

  len = sizeof(data);
  if(len % 2)
    error("Odd-sized array passed to load_unq_help (" + path + ")!");

  for(ctr = 0; ctr < len; ctr+=2) {
    if(data[ctr] == "name") {
      if(names) {
	new_unq_entry(path, names, desc, keywords);

	names = desc = nil;
	keywords = nil;
      }

      names = data[ctr + 1];
      if(!names)
	error("Bad name data in " + path);
    } else if(data[ctr] == "desc") {
      if(desc)
	error("Description entry already present in help file " + path);

      desc = data[ctr + 1];
      if(!desc)
	error("Bad description data in " + path);
    } else if(data[ctr] == "keywords") {
      if(keywords)
	error("Keywords entry already present in help file " + path);

      keywords = data[ctr + 1];
      if(!keywords)
	error("Bad keyword data in " + path);
    } else {
      error("Unrecognized tag " + STRINGD->mixed_sprint(data[ctr])
	    + " parsing help file...");
    }
  }

  if(names)
    new_unq_entry(path, names, desc, keywords);
}

static void load_helpfiles(void) {
  int     ctr;
  string* files_this_time;

  files_this_time = files_to_load[..(FILES_PER_ITER-1)];
  files_to_load = files_to_load[FILES_PER_ITER..];

  load_callout = -1;
  if(sizeof(files_to_load) - FILES_PER_ITER > 0) {
    load_callout = call_out("load_helpfiles", 0);
    if(load_callout < 0)
      LOGD->write_syslog("Couldn't schedule call_out for helpfile loading!",
			 LOG_ERR);
  }

  for(ctr = 0; ctr < FILES_PER_ITER; ctr++) {
    new_help_file(files_this_time[ctr]);
  }

}
