/**
   * Gets a reply to an input. Assumes that the input has already had all necessary substitutions
   * and pre-processing performed, and that the input is a single sentence.
   *
   * @param input the input sentence
   * @param that the input that value
   * @param topic the input topic value
   * @param userid the userid requesting the reply
   * @param botid
   * @return the reply to the input sentence
   */
  private static String getReply(
      String input, String that, String topic, String userid, String botid) {
    // Push the input onto the <input/> stack.
    PredicateMaster.push(INPUT, input, userid, botid);

    // Create a new TemplateParser.
    TemplateParser parser;
    try {
      parser = new TemplateParser(input, userid, botid);
    } catch (TemplateParserException e) {
      throw new DeveloperError(e);
    }

    String reply = null;
    try {
      reply = getMatchResult(input, that, topic, userid, botid, parser);
    } catch (DeveloperError e) {
      Log.devfail(e);
      Log.devfail("Exiting due to developer error.", Log.ERROR);
      System.exit(1);
    } catch (UserError e) {
      Log.userfail(e);
      Log.devfail("Exiting due to user error.", Log.ERROR);
      System.exit(1);
    } catch (RuntimeException e) {
      Log.devfail(e);
      Log.devfail("Exiting due to unforeseen runtime exception.", Log.ERROR);
      System.exit(1);
    }
    if (reply == null) {
      Log.devfail("getMatchReply generated a null reply!", Log.ERROR);
      System.exit(1);
    }

    // Push the reply onto the <that/> stack.
    PredicateMaster.push(THAT, reply, userid, botid);

    return Toolkit.filterWhitespace(reply);
  }
  /**
   * Gets the match result from the Graphmaster.
   *
   * @param input
   * @param that
   * @param topic
   * @param userid
   * @param botid
   * @param parser
   * @param timeout whether to control the match attempt with a timeout thread
   */
  private static String getMatchResult(
      String input, String that, String topic, String userid, String botid, TemplateParser parser) {
    // Always show the input path (in any case, if match trace is on).
    if (SHOW_MATCH_TRACE) {
      Trace.userinfo(
          PredicateMaster.get(Globals.getClientNamePredicate(), userid, botid)
              + '>'
              + SPACE
              + input
              + SPACE
              + Graphmaster.PATH_SEPARATOR
              + SPACE
              + that
              + SPACE
              + Graphmaster.PATH_SEPARATOR
              + SPACE
              + topic
              + SPACE
              + Graphmaster.PATH_SEPARATOR
              + SPACE
              + botid);
    }

    // Create a case-insensitive pattern-fitted version of the input.
    String inputIgnoreCase = InputNormalizer.patternFitIgnoreCase(input);

    Match match = null;

    try {
      match = Graphmaster.match(InputNormalizer.patternFitIgnoreCase(input), that, topic, botid);
    } catch (NoMatchException e) {
      Log.userinfo(e.getMessage(), Log.CHAT);
      return EMPTY_STRING;
    }

    if (match == null) {
      Log.userinfo("No match found for input \"" + input + "\".", Log.CHAT);
      return EMPTY_STRING;
    }

    if (SHOW_MATCH_TRACE) {
      Trace.userinfo(LABEL_MATCH + match.getPath());
      Trace.userinfo(LABEL_FILENAME + QUOTE_MARK + match.getFileName() + QUOTE_MARK);
    }

    ArrayList stars = match.getInputStars();
    if (stars.size() > 0) {
      parser.setInputStars(stars);
    }

    stars = match.getThatStars();
    if (stars.size() > 0) {
      parser.setThatStars(stars);
    }

    stars = match.getTopicStars();
    if (stars.size() > 0) {
      parser.setTopicStars(stars);
    }

    String template = match.getTemplate();
    String reply = null;

    try {
      reply = parser.processResponse(template);
    } catch (ProcessorException e) {
      // Log the error message.
      Log.userinfo(e.getMessage(), Log.ERROR);

      // Set response to empty string.
      return EMPTY_STRING;
    }

    // Record activation, if targeting is in use.
    // Needs review in light of multi-bot update
    if (USE_TARGETING) {
      Nodemapper matchNodemapper = match.getNodemapper();
      if (matchNodemapper == null) {
        Trace.devinfo("Match nodemapper is null!");
      } else {
        Set activations = (Set) matchNodemapper.get(Graphmaster.ACTIVATIONS);
        if (activations == null) {
          activations = new HashSet();
        }
        String path =
            match.getPath()
                + SPACE
                + Graphmaster.PATH_SEPARATOR
                + SPACE
                + inputIgnoreCase
                + SPACE
                + Graphmaster.PATH_SEPARATOR
                + SPACE
                + that
                + SPACE
                + Graphmaster.PATH_SEPARATOR
                + SPACE
                + topic
                + SPACE
                + Graphmaster.PATH_SEPARATOR
                + SPACE
                + botid
                + SPACE
                + Graphmaster.PATH_SEPARATOR
                + SPACE
                + reply;
        if (!activations.contains(path)) {
          activations.add(path);
          match.getNodemapper().put(Graphmaster.ACTIVATIONS, activations);
          Graphmaster.activatedNode(match.getNodemapper());
        }
      }
    }
    return reply;
  }