<filename>src/net/sourceforge/kolmafia/request/DwarfFactoryRequest.java
package net.sourceforge.kolmafia.request;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.AdventureResult;
import net.sourceforge.kolmafia.KoLAdventure;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.objectpool.IntegerPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.persistence.ItemDatabase;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.session.InventoryManager;
import net.sourceforge.kolmafia.utilities.StringUtilities;

public class DwarfFactoryRequest extends GenericRequest {
  public static final Pattern RUNE_PATTERN =
      Pattern.compile("title=\"Dwarf (Digit|Word) Rune (.)\"");
  public static final Pattern ITEMDESC_PATTERN = Pattern.compile("descitem\\((\\d*)\\)");
  public static final Pattern MEAT_PATTERN = Pattern.compile("You (gain|lose) (\\d*) Meat");
  public static final Pattern HOPPER_PATTERN =
      Pattern.compile("<p>It currently contains (\\d+) ([^.]*)\\.</p>");

  private static final int[] ITEMS =
      new int[] {
        ItemPool.SPRING,
        ItemPool.SPROCKET,
        ItemPool.COG,
        ItemPool.MINERS_HELMET,
        ItemPool.MINERS_PANTS,
        ItemPool.MATTOCK,
        ItemPool.LINOLEUM_ORE,
        ItemPool.ASBESTOS_ORE,
        ItemPool.CHROME_ORE,
        ItemPool.DWARF_BREAD,
        ItemPool.LUMP_OF_COAL,
      };

  private static final int[] ORES =
      new int[] {
        ItemPool.LINOLEUM_ORE, ItemPool.ASBESTOS_ORE, ItemPool.CHROME_ORE, ItemPool.LUMP_OF_COAL,
      };

  private static final int[] EQUIPMENT =
      new int[] {
        ItemPool.MINERS_HELMET, ItemPool.MINERS_PANTS, ItemPool.MATTOCK,
      };

  private static DwarfNumberTranslator digits = null;
  private static final int[] hopperCount = new int[4];
  private static final int[] inventoryCount = new int[4];

  public static void reset() {
    DwarfFactoryRequest.digits = null;
    DwarfFactoryRequest.hopperCount[0] = -1;
    DwarfFactoryRequest.hopperCount[1] = -1;
    DwarfFactoryRequest.hopperCount[2] = -1;
    DwarfFactoryRequest.hopperCount[3] = -1;
  }

  public static boolean valid() {
    return DwarfFactoryRequest.digits != null && DwarfFactoryRequest.digits.valid();
  }

  public DwarfFactoryRequest() {
    super("dwarffactory.php");
  }

  public DwarfFactoryRequest(final String action) {
    this();
    this.addFormField("action", action);
  }

  @Override
  public void processResults() {
    DwarfFactoryRequest.parseResponse(this.getURLString(), this.responseText);
  }

  public static final void parseResponse(final String urlString, final String responseText) {
    if (!urlString.startsWith("dwarffactory.php")) {
      return;
    }

    Matcher matcher = GenericRequest.ACTION_PATTERN.matcher(urlString);
    String action = matcher.find() ? matcher.group(1) : null;

    // We have nothing special to do for simple visits.

    if (action == null) {
      return;
    }

    if (action.equals("ware")) {
      Matcher runeMatcher = DwarfFactoryRequest.getRuneMatcher(responseText);
      String rune1 = DwarfFactoryRequest.getRune(runeMatcher);
      String rune2 = DwarfFactoryRequest.getRune(runeMatcher);
      String rune3 = DwarfFactoryRequest.getRune(runeMatcher);
      int itemId = DwarfFactoryRequest.getItemId(responseText);
      DwarfFactoryRequest.setItemRunes(itemId, rune1, rune2, rune3);
      return;
    }

    if (action.equals("dodice")) {
      Matcher meatMatcher = MEAT_PATTERN.matcher(responseText);
      if (!meatMatcher.find()) {
        return;
      }

      boolean won = meatMatcher.group(1).equals("gain");
      int meat = StringUtilities.parseInt(meatMatcher.group(2)) / 7;
      String meat7 = String.valueOf(meat / 7) + meat % 7;

      Matcher runeMatcher = DwarfFactoryRequest.getRuneMatcher(responseText);
      String first =
          DwarfFactoryRequest.getRune(runeMatcher) + DwarfFactoryRequest.getRune(runeMatcher);
      String second =
          DwarfFactoryRequest.getRune(runeMatcher) + DwarfFactoryRequest.getRune(runeMatcher);

      String message;
      if (won) {
        message = second + "-" + first + "=" + meat7;
      } else {
        message = first + "-" + second + "=" + meat7;
      }

      RequestLogger.printLine(message);
      RequestLogger.updateSessionLog(message);

      DwarfFactoryRequest.addDieRoll(message);
      return;
    }
  }

  public static void clearHoppers() {
    DwarfFactoryRequest.hopperCount[0] = 0;
    DwarfFactoryRequest.hopperCount[1] = 0;
    DwarfFactoryRequest.hopperCount[2] = 0;
    DwarfFactoryRequest.hopperCount[3] = 0;
  }

  public static final void decorate(final String urlString, final StringBuffer buffer) {
    if (!urlString.startsWith("dwarffactory.php")) {
      return;
    }

    Matcher matcher = GenericRequest.ACTION_PATTERN.matcher(urlString);
    String action = matcher.find() ? matcher.group(1) : null;

    if (action == null) {
      return;
    }

    if (action.equals("dodice")) {
      if (!DwarfFactoryRequest.valid()) {
        return;
      }

      // Find the end of the table following the "you
      // win/lose xx meat" message.
      int index = buffer.indexOf("meat.gif");
      index = buffer.indexOf("</center>", index);
      buffer.insert(index, "The dwarf digit code has been solved!");
    }
  }

  private static void setItemRunes(
      final int itemId, final String rune1, final String rune2, final String rune3) {
    KoLCharacter.ensureUpdatedDwarfFactory();

    // KoL will not give us an item if it thinks we are overdrunk
    if (itemId == -1) {
      return;
    }

    // If we are looking at runes for ore or equipment and we know
    // the complete list of ore runes or equipment runes because
    // we've looked at an office item, we can eliminate runes not
    // on the appropriate list.

    String typeRunes = "";
    String ores = null;
    String equipment = null;

    switch (itemId) {
      case ItemPool.LINOLEUM_ORE:
      case ItemPool.ASBESTOS_ORE:
      case ItemPool.CHROME_ORE:
      case ItemPool.LUMP_OF_COAL:
        ores = Preferences.getString("lastDwarfOreRunes");
        if (ores.length() == 4) {
          typeRunes = ores;
        }
        break;
      case ItemPool.MINERS_HELMET:
      case ItemPool.MINERS_PANTS:
      case ItemPool.MATTOCK:
        equipment = Preferences.getString("lastDwarfEquipmentRunes");
        if (equipment.length() == 3) {
          typeRunes = equipment;
        }
        break;
    }

    String setting = "lastDwarfFactoryItem" + itemId;
    String oldRunes = Preferences.getString(setting);
    String newRunes = "";

    if (typeRunes.equals("") || typeRunes.indexOf(rune1) != -1) {
      if (oldRunes.equals("") || oldRunes.indexOf(rune1) != -1) {
        newRunes += rune1;
      }
    }

    if (typeRunes.equals("") || typeRunes.indexOf(rune2) != -1) {
      if (oldRunes.equals("") || oldRunes.indexOf(rune2) != -1) {
        newRunes += rune2;
      }
    }

    if (typeRunes.equals("") || typeRunes.indexOf(rune3) != -1) {
      if (oldRunes.equals("") || oldRunes.indexOf(rune3) != -1) {
        newRunes += rune3;
      }
    }

    // Eliminate any runes which definitively belong to another item.
    for (int i = 0; i < ITEMS.length; ++i) {
      int id = ITEMS[i];
      if (id == itemId) {
        continue;
      }

      String value = Preferences.getString("lastDwarfFactoryItem" + id);

      if (value.length() == 1 && newRunes.indexOf(value) != -1) {
        newRunes = StringUtilities.globalStringDelete(newRunes, value);
      }
    }

    DwarfFactoryRequest.setItemRunes(itemId, newRunes);
  }

  private static void checkForLastRune(String runes, int[] items, String type) {
    if (items == null) {
      return;
    }

    int candidate = 0;

    for (int i = 0; i < items.length; ++i) {
      int itemId = items[i];
      String setting = "lastDwarfFactoryItem" + itemId;
      String value = Preferences.getString(setting);

      if (value.length() != 1) {
        // Unidentified item
        if (candidate != 0) {
          // Already another
          return;
        }
        candidate = itemId;
        continue;
      }

      // This is an identified rune. Remove from master list.
      runes = StringUtilities.globalStringDelete(runes, value);
    }

    // If we get here, there is at most one item on the list we've
    // not identified.
    if (candidate != 0) {
      DwarfFactoryRequest.setItemRunes(candidate, runes);
      KoLmafia.updateDisplay("All " + type + " have been identified!");
    }
  }

  public static void setItemRunes(final int itemId, final String runes) {
    String setting = "lastDwarfFactoryItem" + itemId;
    Preferences.setString(setting, runes);

    if (runes.length() > 1) {
      return;
    }

    // See if we've identified the penultimate item and can thus
    // deduce the final item.
    switch (itemId) {
      case ItemPool.LINOLEUM_ORE:
      case ItemPool.ASBESTOS_ORE:
      case ItemPool.CHROME_ORE:
      case ItemPool.LUMP_OF_COAL:
        String ores = Preferences.getString("lastDwarfOreRunes");
        if (ores.length() == 4) {
          DwarfFactoryRequest.checkForLastRune(ores, DwarfFactoryRequest.ORES, "ores");
        }
        break;
      case ItemPool.MINERS_HELMET:
      case ItemPool.MINERS_PANTS:
      case ItemPool.MATTOCK:
        String equipment = Preferences.getString("lastDwarfEquipmentRunes");
        if (equipment.length() == 3) {
          DwarfFactoryRequest.checkForLastRune(
              equipment, DwarfFactoryRequest.EQUIPMENT, "pieces of equipment");
        }
        break;
    }

    // If the length is 1, that rune has been matched with an
    // item. Therefore, if it appears in the list of runes for
    // another item, it can't be the match for that item, too, and
    // can be removed from that list.

    DwarfFactoryRequest.pruneItemRunes(itemId, runes);
  }

  private static void pruneItemRunes(final int id, final String rune) {
    for (int i = 0; i < ITEMS.length; ++i) {
      int itemId = ITEMS[i];
      if (id == itemId) {
        continue;
      }

      DwarfFactoryRequest.eliminateItemRune(itemId, rune);
    }
  }

  private static void eliminateItemRune(final int itemId, final String rune) {
    String setting = "lastDwarfFactoryItem" + itemId;
    String value = Preferences.getString(setting);

    if (value.length() == 1) {
      return;
    }

    if (value.indexOf(rune) == -1) {
      return;
    }

    value = StringUtilities.globalStringDelete(value, rune);
    Preferences.setString(setting, value);

    if (value.length() > 1) {
      return;
    }

    // We've identified another item. Recurse!
    DwarfFactoryRequest.pruneItemRunes(itemId, value);
  }

  public static final void setHopperRune(final int hopper, final String responseText) {
    KoLCharacter.ensureUpdatedDwarfFactory();

    // Parse the rune from the response text
    String rune = DwarfFactoryRequest.getRune(responseText);

    // Associate this rune with this hopper
    Preferences.setString("lastDwarfHopper" + hopper, rune);

    // Add to list of known ore runes
    DwarfFactoryRequest.setOreRune(rune);

    // See how much ore is currently in the hopper
    if (responseText.indexOf("It is currently empty") != -1) {
      hopperCount[hopper - 1] = 0;
      return;
    }

    Matcher matcher = DwarfFactoryRequest.HOPPER_PATTERN.matcher(responseText);
    if (!matcher.find()) {
      return;
    }

    int count = StringUtilities.parseInt(matcher.group(1));
    hopperCount[hopper - 1] = count;
  }

  public static void setOreRune(final String rune) {
    String runes = Preferences.getString("lastDwarfOreRunes");
    if (runes.indexOf(rune) != -1) {
      return;
    }

    // It's a new ore. Add it to the list of ores.
    runes += rune;
    Preferences.setString("lastDwarfOreRunes", runes);

    // See if we can auto-identify an ore
    if (runes.length() == 4) {
      DwarfFactoryRequest.checkForLastRune(runes, DwarfFactoryRequest.ORES, "ores");
    }

    // Prune this rune from any non-ores
    for (int i = 0; i < ITEMS.length; ++i) {
      int itemId = ITEMS[i];

      switch (itemId) {
        case ItemPool.LINOLEUM_ORE:
        case ItemPool.ASBESTOS_ORE:
        case ItemPool.CHROME_ORE:
        case ItemPool.LUMP_OF_COAL:
          continue;
      }

      DwarfFactoryRequest.eliminateItemRune(itemId, rune);
    }
  }

  private static void setEquipmentRune(final String rune) {
    String runes = Preferences.getString("lastDwarfEquipmentRunes");
    if (runes.indexOf(rune) != -1) {
      return;
    }

    // It's a new piece of equipment. Add it to the list of equipment.
    runes += rune;
    Preferences.setString("lastDwarfEquipmentRunes", runes);

    // See if we can auto-identify some equipment
    if (runes.length() == 3) {
      DwarfFactoryRequest.checkForLastRune(
          runes, DwarfFactoryRequest.EQUIPMENT, "pieces of equipment");
    }

    // Prune this rune from any non-equipment
    for (int i = 0; i < ITEMS.length; ++i) {
      int itemId = ITEMS[i];

      switch (itemId) {
        case ItemPool.MINERS_HELMET:
        case ItemPool.MINERS_PANTS:
        case ItemPool.MATTOCK:
          continue;
      }

      DwarfFactoryRequest.eliminateItemRune(itemId, rune);
    }
  }

  private static Matcher getRuneMatcher(final String responseText) {
    return RUNE_PATTERN.matcher(responseText);
  }

  public static String getRune(final String responseText) {
    Matcher matcher = DwarfFactoryRequest.getRuneMatcher(responseText);
    return DwarfFactoryRequest.getRune(matcher);
  }

  public static String getRunes(final String responseText) {
    Matcher matcher = DwarfFactoryRequest.getRuneMatcher(responseText);
    StringBuffer resultBuilder = new StringBuffer();
    while (matcher.find()) {
      resultBuilder.append(matcher.group(2));
    }

    return resultBuilder.toString();
  }

  private static String getRune(final Matcher matcher) {
    if (!matcher.find()) {
      return "";
    }

    return matcher.group(2);
  }

  private static String getDigits() {
    return Preferences.getString("lastDwarfDigitRunes");
  }

  private static void setDigits() {
    if (DwarfFactoryRequest.digits == null) {
      return;
    }
    String digits = DwarfFactoryRequest.digits.digitString();
    Preferences.setString("lastDwarfDigitRunes", digits);
  }

  public static void setDigits(String digits) {
    Preferences.setString("lastDwarfDigitRunes", digits);
    DwarfFactoryRequest.digits = new DwarfNumberTranslator(digits);
  }

  public static int parseNumber(final String runes) {
    if (DwarfFactoryRequest.digits == null) {
      DwarfFactoryRequest.digits = new DwarfNumberTranslator();
    }
    return DwarfFactoryRequest.digits.parseNumber(runes);
  }

  public static int parseNumber(final String runes, final String digits) {
    DwarfNumberTranslator translator = new DwarfNumberTranslator(digits);
    return translator.parseNumber(runes);
  }

  public static void useUnlaminatedItem(final int itemId, final String responseText) {
    Matcher matcher = DwarfFactoryRequest.getRuneMatcher(responseText);
    StringBuffer runesBuilder = new StringBuffer();
    int count = 0;

    while (matcher.find()) {
      String rune = matcher.group(2);

      if (count++ == 0) {
        DwarfFactoryRequest.setEquipmentRune(rune);
        runesBuilder.append(rune);
        continue;
      }

      String type = matcher.group(1);
      if (type.equals("Word")) {
        DwarfFactoryRequest.setOreRune(rune);
        runesBuilder.append(',');
      }
      runesBuilder.append(rune);
    }
    Preferences.setString("lastDwarfOfficeItem" + itemId, runesBuilder.toString());
  }

  public static void useLaminatedItem(final int itemId, final String responseText) {
    Matcher matcher = DwarfFactoryRequest.getRuneMatcher(responseText);
    StringBuffer runesBuilder = new StringBuffer();
    int count = 0;

    while (matcher.find()) {
      String rune = matcher.group(2);

      if (count++ == 0) {
        DwarfFactoryRequest.setOreRune(rune);
        runesBuilder.append(rune);
        continue;
      }

      if (count == 2) {
        // Skip rune for "gauges"
        continue;
      }

      String type = matcher.group(1);
      if (type.equals("Word")) {
        DwarfFactoryRequest.setEquipmentRune(rune);
        runesBuilder.append(',');
      }
      runesBuilder.append(rune);
    }
    Preferences.setString("lastDwarfOfficeItem" + itemId, runesBuilder.toString());
  }

  private static int getItemId(final String responseText) {
    Matcher matcher = ITEMDESC_PATTERN.matcher(responseText);
    if (!matcher.find()) {
      return -1;
    }

    return ItemDatabase.getItemIdFromDescription(matcher.group(1));
  }

  public static final boolean registerRequest(final String urlString) {
    if (!urlString.startsWith("dwarffactory.php")) {
      return false;
    }

    Matcher matcher = GenericRequest.ACTION_PATTERN.matcher(urlString);
    String action = matcher.find() ? matcher.group(1) : null;

    // We have nothing special to do for simple visits.

    if (action == null) {
      return true;
    }

    if (action.equals("ware")) {
      String message = "[" + KoLAdventure.getAdventureCount() + "] Dwarven Factory Warehouse";

      RequestLogger.printLine("");
      RequestLogger.printLine(message);

      RequestLogger.updateSessionLog();
      RequestLogger.updateSessionLog(message);

      return true;
    }

    if (action.equals("dorm")) {
      String message = "Visiting the Dwarven Factory Dormitory";

      RequestLogger.printLine("");
      RequestLogger.printLine(message);

      RequestLogger.updateSessionLog();
      RequestLogger.updateSessionLog(message);
      return true;
    }

    if (action.equals("dodice") || action.equals("nodice") || action.equals("nonodice")) {
      return true;
    }

    return false;
  }

  // Module to parse special messages from Dwarvish War Uniform items
  // There is currently an extra space after the last 'really'
  private static final Pattern DWARF_MATTOCK_PATTERN =
      Pattern.compile("<p>Your mattock glows ((really )*) ?bright blue\\.</p>");

  public static Matcher hpMessage(final CharSequence responseText) {
    Matcher mattockMatcher = DwarfFactoryRequest.DWARF_MATTOCK_PATTERN.matcher(responseText);
    return mattockMatcher.find() ? mattockMatcher : null;
  }

  public static int deduceHP(final CharSequence responseText) {
    Matcher mattockMatcher = DwarfFactoryRequest.hpMessage(responseText);
    if (mattockMatcher == null) {
      return 0;
    }
    return DwarfFactoryRequest.deduceHP(mattockMatcher);
  }

  public static int deduceHP(final Matcher mattockMatcher) {
    return mattockMatcher.end(1) - mattockMatcher.start(1);
  }

  private static final Pattern DWARF_HELMET_PATTERN =
      Pattern.compile("<p>A small crystal lens flips down.*?</p>");

  public static Matcher attackMessage(final CharSequence responseText) {
    Matcher helmetMatcher = DwarfFactoryRequest.DWARF_HELMET_PATTERN.matcher(responseText);
    return helmetMatcher.find() ? helmetMatcher : null;
  }

  public static int deduceAttack(final CharSequence responseText) {
    Matcher helmetMatcher = DwarfFactoryRequest.attackMessage(responseText);
    if (helmetMatcher == null) {
      return 0;
    }
    return DwarfFactoryRequest.deduceAttack(helmetMatcher);
  }

  public static int deduceAttack(final Matcher helmetMatcher) {
    String runes = DwarfFactoryRequest.getRunes(helmetMatcher.group(0));
    return DwarfFactoryRequest.parseNumber(runes);
  }

  // A little light on your sporran lights up <color>
  //
  // Two little lights light up on your sporran -- a <color> one and a <color> one.
  //
  // Three little lights light up <color>, <color>, and <color> on your sporran.
  //
  // Your sporran lights up with a series of four little lights: <color>, <color>, <color>, and
  // <color>.
  //
  // A bunch of little lights on your sporran start flashing random colors like there's a rave on
  // your crotch.

  public static final Pattern DWARF_KILT_PATTERN = Pattern.compile("<p>.*?our sporran.*?</p>");

  public static Matcher defenseMessage(final CharSequence responseText) {
    Matcher kiltMatcher = DwarfFactoryRequest.DWARF_KILT_PATTERN.matcher(responseText);
    return kiltMatcher.find() ? kiltMatcher : null;
  }

  public static int deduceDefense(final CharSequence responseText) {
    Matcher kiltMatcher = DwarfFactoryRequest.defenseMessage(responseText);
    if (kiltMatcher == null) {
      return 0;
    }
    return DwarfFactoryRequest.deduceDefense(kiltMatcher);
  }

  public static final Pattern COLOR_PATTERN =
      Pattern.compile("(red|orange|yellow|green|blue|indigo|violet)");

  public static int deduceDefense(final Matcher kiltMatcher) {
    if (kiltMatcher.group(0).indexOf("rave on your crotch") != -1) {
      return 99999;
    }

    Matcher matcher = DwarfFactoryRequest.COLOR_PATTERN.matcher(kiltMatcher.group(0));
    int number = 0;

    while (matcher.find()) {
      String color = matcher.group(1);
      int digit = -1;
      if (color.equals("red")) {
        digit = 0;
      } else if (color.equals("orange")) {
        digit = 1;
      } else if (color.equals("yellow")) {
        digit = 2;
      } else if (color.equals("green")) {
        digit = 3;
      } else if (color.equals("blue")) {
        digit = 4;
      } else if (color.equals("indigo")) {
        digit = 5;
      } else if (color.equals("violet")) {
        digit = 6;
      } else {
        return -1;
      }
      number = (number * 7) + digit;
    }
    return number;
  }

  // Module to check whether you've found everything you need

  public static boolean check(final boolean use) {
    StringBuffer output = new StringBuffer();

    // Check the office items
    DwarfFactoryRequest.checkOrUse(ItemPool.SMALL_LAMINATED_CARD, use, output);
    DwarfFactoryRequest.checkOrUse(ItemPool.LITTLE_LAMINATED_CARD, use, output);
    DwarfFactoryRequest.checkOrUse(ItemPool.NOTBIG_LAMINATED_CARD, use, output);
    DwarfFactoryRequest.checkOrUse(ItemPool.UNLARGE_LAMINATED_CARD, use, output);
    DwarfFactoryRequest.checkOrUse(ItemPool.DWARVISH_DOCUMENT, use, output);
    DwarfFactoryRequest.checkOrUse(ItemPool.DWARVISH_PAPER, use, output);
    DwarfFactoryRequest.checkOrUse(ItemPool.DWARVISH_PARCHMENT, use, output);

    // Check the hoppers
    DwarfFactoryRequest.checkHopper(1, use, output);
    DwarfFactoryRequest.checkHopper(2, use, output);
    DwarfFactoryRequest.checkHopper(3, use, output);
    DwarfFactoryRequest.checkHopper(4, use, output);

    // Check the ores and equipment
    DwarfFactoryRequest.checkItem(ItemPool.MINERS_HELMET, use, output);
    DwarfFactoryRequest.checkItem(ItemPool.MINERS_PANTS, use, output);
    DwarfFactoryRequest.checkItem(ItemPool.MATTOCK, use, output);
    DwarfFactoryRequest.checkItem(ItemPool.LINOLEUM_ORE, use, output);
    DwarfFactoryRequest.checkItem(ItemPool.ASBESTOS_ORE, use, output);
    DwarfFactoryRequest.checkItem(ItemPool.CHROME_ORE, use, output);
    DwarfFactoryRequest.checkItem(ItemPool.LUMP_OF_COAL, use, output);

    String text = output.toString();
    if (text.length() > 0) {
      RequestLogger.printLine(text);
      return false;
    }

    return true;
  }

  public static void checkOrUse(final int itemId, final boolean use, final StringBuffer output) {
    if (!InventoryManager.hasItem(itemId)) {
      output.append(
          "You do not have the " + ItemDatabase.getItemName(itemId) + KoLConstants.LINE_BREAK);
      return;
    }

    if (!use || !Preferences.getString("lastDwarfOfficeItem" + itemId).equals("")) {
      return;
    }

    RequestThread.postRequest(UseItemRequest.getInstance(ItemPool.get(itemId, 1)));
  }

  public static void checkHopper(final int hopper, final boolean use, final StringBuffer output) {
    if (!use || DwarfFactoryRequest.hopperCount[hopper - 1] != -1) {
      return;
    }

    RequestLogger.printLine("Visiting hopper #" + hopper + " to check ore level");
    RequestThread.postRequest(new DwarfContraptionRequest("hopper" + (hopper - 1)));
  }

  public static void checkItem(final int itemId, final boolean use, final StringBuffer output) {
    if (Preferences.getString("lastDwarfFactoryItem" + itemId).length() != 1) {
      output.append(
          "You not yet identified the "
              + ItemDatabase.getItemName(itemId)
              + KoLConstants.LINE_BREAK);
      return;
    }
  }

  // Module to solve the digit code

  public static void solve() {
    // If we don't currently have a digit translator, create one
    // from the preference
    if (DwarfFactoryRequest.digits == null) {
      DwarfFactoryRequest.digits = new DwarfNumberTranslator();
    }

    // If it's valid, we're golden
    if (DwarfFactoryRequest.digits.valid()) {
      return;
    }

    // Here's where we solve it.

    // Step 0: get the unlaminated numbers
    String[] unlaminated = getUnlaminatedNumbers();
    for (int i = 0; i < unlaminated.length; ++i) {
      DwarfFactoryRequest.digits.addNumber(unlaminated[i]);
    }

    // Step 1: try to deduce what we can from the laminated items
    String[] laminated = getLaminatedNumbers();
    for (int i = 0; i < laminated.length; ++i) {
      DwarfFactoryRequest.digits.addNumber(laminated[i]);
    }
    DwarfFactoryRequest.digits.analyzeNumbers();

    // Step 2: iterate over saved dice rules, deducing what we can
    String[] rolls = DwarfFactoryRequest.getDiceRolls();
    for (int i = 0; i < rolls.length; ++i) {
      DwarfFactoryRequest.digits.addRoll(rolls[i]);
    }
    DwarfFactoryRequest.digits.analyzeRolls();

    // Save the current digit string, complete or not
    DwarfFactoryRequest.setDigits();

    if (DwarfFactoryRequest.digits.valid()) {
      KoLmafia.updateDisplay("Dwarf digit code solved!");
    } else {
      RequestLogger.printLine("Unable to solve digit code. Get more data!");
    }
  }

  private static String[] getUnlaminatedNumbers() {
    ArrayList<String> numbers = new ArrayList<>();
    // lastDwarfOfficeItem3212=H,QEG,BFD,OJI,JED
    DwarfFactoryRequest.getItemNumbers(numbers, ItemPool.DWARVISH_DOCUMENT, 4);
    DwarfFactoryRequest.getItemNumbers(numbers, ItemPool.DWARVISH_PAPER, 4);
    DwarfFactoryRequest.getItemNumbers(numbers, ItemPool.DWARVISH_PARCHMENT, 4);
    return numbers.toArray(new String[numbers.size()]);
  }

  private static String[] getLaminatedNumbers() {
    ArrayList<String> numbers = new ArrayList<>();
    // lastDwarfOfficeItem3208=B,HGIG,MGDE,PJD
    DwarfFactoryRequest.getItemNumbers(numbers, ItemPool.SMALL_LAMINATED_CARD, 3);
    DwarfFactoryRequest.getItemNumbers(numbers, ItemPool.LITTLE_LAMINATED_CARD, 3);
    DwarfFactoryRequest.getItemNumbers(numbers, ItemPool.NOTBIG_LAMINATED_CARD, 3);
    DwarfFactoryRequest.getItemNumbers(numbers, ItemPool.UNLARGE_LAMINATED_CARD, 3);
    return numbers.toArray(new String[numbers.size()]);
  }

  private static void getItemNumbers(
      final ArrayList<String> list, final int itemId, final int count) {
    String setting = DwarfFactoryRequest.getItemSetting(itemId);
    String[] splits = setting.split(",");
    if (splits.length != count + 1) {
      return;
    }

    for (int i = 1; i <= count; ++i) {
      list.add(splits[i].substring(1));
    }
  }

  private static String getItemSetting(int itemId) {
    String settingName = "lastDwarfOfficeItem" + itemId;
    String setting = Preferences.getString(settingName);
    if (!setting.equals("")) {
      return setting;
    }

    if (InventoryManager.hasItem(itemId)) {
      RequestThread.postRequest(UseItemRequest.getInstance(ItemPool.get(itemId, 1)));
    }

    return Preferences.getString(settingName);
  }

  private static void addDieRoll(final String message) {
    // Add the die roll to the list of saved rolls

    KoLCharacter.ensureUpdatedDwarfFactory();
    String dice = Preferences.getString("lastDwarfDiceRolls");
    Preferences.setString("lastDwarfDiceRolls", dice + message + ":");

    // Try to crack the digit code
    DwarfFactoryRequest.solve();
  }

  private static String[] getDiceRolls() {
    String rolls = Preferences.getString("lastDwarfDiceRolls");
    return rolls.split(":");
  }

  // Module to report on what we've gleaned about the factory quest

  public static void report() {
    DwarfFactoryRequest.solve();
    if (DwarfFactoryRequest.digits == null) {
      return;
    }
    DwarfFactoryRequest.report(DwarfFactoryRequest.digits);
  }

  public static void report(final String digits) {
    if (digits.length() != 7) {
      RequestLogger.printLine("Digit string must have 7 characters");
      return;
    }
    DwarfNumberTranslator translator = new DwarfNumberTranslator(digits);
    DwarfFactoryRequest.report(translator);
  }

  private static void report(final DwarfNumberTranslator digits) {
    if (!digits.valid()) {
      RequestLogger.printLine("Invalid or incomplete digit set");
      return;
    }

    if (!DwarfFactoryRequest.check(true)) {
      return;
    }

    FactoryData data = new FactoryData(digits);
    data.setInventoryCounts();

    String output =
        "<table border=2 cols=6>"
            +

            // Put in a header
            "<tr>"
            + "<td rowspan=3 colspan=2></td>"
            + "<td align=center>Hopper #1</td>"
            + "<td align=center>Hopper #2</td>"
            + "<td align=center>Hopper #3</td>"
            + "<td align=center>Hopper #4</td>"
            + "</tr>"
            + "<tr>"
            + "<td align=center>"
            + data.getHopperOre(0)
            + "</td>"
            + "<td align=center>"
            + data.getHopperOre(1)
            + "</td>"
            + "<td align=center>"
            + data.getHopperOre(2)
            + "</td>"
            + "<td align=center>"
            + data.getHopperOre(3)
            + "</td>"
            + "</tr>"
            + "<tr>"
            + "<td align=center>"
            + data.getHopperOreCounts(0)
            + "</td>"
            + "<td align=center>"
            + data.getHopperOreCounts(1)
            + "</td>"
            + "<td align=center>"
            + data.getHopperOreCounts(2)
            + "</td>"
            + "<td align=center>"
            + data.getHopperOreCounts(3)
            + "</td>"
            + "</tr>"
            +

            // Add HAT
            "<tr>"
            + "<td align=center rowspan=2>Hat</td>"
            + "<td align=center>Gauges</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.HAT, 0)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.HAT, 1)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.HAT, 2)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.HAT, 3)
            + "</td>"
            + "</tr>"
            + "<tr>"
            + "<td align=center>Ores</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.HAT, 0)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.HAT, 1)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.HAT, 2)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.HAT, 3)
            + "</td>"
            + "</tr>"
            +

            // Add PANTS
            "<tr>"
            + "<td align=center rowspan=2>Pants</td>"
            + "<td align=center>Gauges</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.PANTS, 0)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.PANTS, 1)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.PANTS, 2)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.PANTS, 3)
            + "</td>"
            + "</tr>"
            + "<tr>"
            + "<td align=center>Ores</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.PANTS, 0)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.PANTS, 1)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.PANTS, 2)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.PANTS, 3)
            + "</td>"
            + "</tr>"
            +

            // Add WEAPON
            "<tr>"
            + "<td align=center rowspan=2>Weapon</td>"
            + "<td align=center>Gauges</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.WEAPON, 0)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.WEAPON, 1)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.WEAPON, 2)
            + "</td>"
            + "<td align=center>"
            + data.getGaugeSetting(FactoryData.WEAPON, 3)
            + "</td>"
            + "</tr>"
            + "<tr>"
            + "<td align=center>Ores</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.WEAPON, 0)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.WEAPON, 1)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.WEAPON, 2)
            + "</td>"
            + "<td align=center>"
            + data.getOreQuantityString(FactoryData.WEAPON, 3)
            + "</td>"
            + "</tr>"
            + "</table>";
    RequestLogger.printLine(output);
    RequestLogger.printLine();
  }

  public static class DwarfNumberTranslator {
    private final Map<Character, Integer> digitMap = new HashMap<>();
    private final Map<Integer, Character> charMap = new HashMap<>();

    public DwarfNumberTranslator() {
      this(DwarfFactoryRequest.getDigits());
    }

    public DwarfNumberTranslator(final String digits) {
      // Make maps from dwarf digit rune to base 7 digit and
      // vice versa
      for (int i = 0; i < digits.length() && i < 7; ++i) {
        char digit = digits.charAt(i);
        if (!Character.isLetter(digit)) {
          continue;
        }

        this.mapCharacter(digit, i);
      }
    }

    private void mapCharacter(final char c, final int i) {
      Character code = Character.valueOf(Character.toUpperCase(c));
      Integer val = IntegerPool.get(i);
      this.mapCharacter(code, val);
    }

    private void mapCharacter(final Character code, final Integer val) {
      this.digitMap.put(code, val);
      this.charMap.put(val, code);
    }

    public Integer getDigit(final Character code) {
      return this.digitMap.get(code);
    }

    public Integer getDigit(final char c) {
      return this.getDigit(Character.valueOf(c));
    }

    public String digitString() {
      StringBuffer valueBuilder = new StringBuffer();
      for (int i = 0; i < 7; ++i) {
        Character code = this.charMap.get(IntegerPool.get(i));
        valueBuilder.append(code == null ? '-' : code.charValue());
      }
      return valueBuilder.toString();
    }

    public boolean valid() {
      return this.digitMap.size() == 7;
    }

    public int parseNumber(final String string) {
      int number = 0;
      for (int i = 0; i < string.length(); ++i) {
        Integer val = this.digitMap.get(Character.valueOf(string.charAt(i)));
        if (val == null) {
          return -1;
        }
        number = (number * 7) + val.intValue();
      }
      return number;
    }

    // Methods for solving the digit code

    private final ArrayList<String> numbers = new ArrayList<>();
    private final ArrayList<Character> digits = new ArrayList<>();

    private void addNewDigit(final char ch) {
      Character digit = Character.valueOf(ch);
      if (!this.digits.contains(digit)) {
        this.digits.add(digit);
      }
    }

    public void addNumber(final String number) {
      // See if it's a new number
      for (int i = 0; i < this.numbers.size(); ++i) {
        String old = this.numbers.get(i);
        if (old.equals(number)) {
          return;
        }
      }

      // Add the new number to the list
      this.numbers.add(number);

      // Add all the digits to the set of digits
      for (int i = 0; i < number.length(); ++i) {
        this.addNewDigit(number.charAt(i));
      }
    }

    // Step 1: Deduce initial digit of three digit numbers from
    // laminated items

    private int numberCount = 0;

    public void analyzeNumbers() {
      // If we've already looked at all the numbers in the
      // array, it's pointless to do it again.

      if (this.numbers.size() == numberCount) {
        return;
      }

      // Save current size of array
      numberCount = this.numbers.size();

      // Gauge numbers encode base-10 numbers from 00 - 99 in
      // base-7. In base 7, numbers greater than 48 take 3
      // digits with the first digit equal to 1. Numbers
      // greater than 97 take 3 digits with the first digit
      // equal to 2. In particular, 98 is 200 and 99 is 201.

      // Therefore, when we have a three digit number, 50
      // times out of 52, it will encode "1", and 2 times out
      // of 52 it will encode "2". If it encodes "1", the
      // following two digits can be any of the 7 digits.

      // Make an array to save each initial digit and the set
      // of all digits that appear after each initial digit

      char[][] matches = new char[2][8];
      int[] counts = new int[2];

      for (int i = 0; i < this.numbers.size(); ++i) {
        String val = this.numbers.get(i);

        // We only deduce digits from 3-digit numbers.
        if (val.length() < 3) {
          continue;
        }

        char d1 = val.charAt(0);
        char d2 = val.charAt(1);
        char d3 = val.charAt(2);

        int off = 0;
        for (int j = 0; j < matches.length; ++j) {
          char match = matches[j][0];
          if (match == 0 || match == d1) {
            off = j;
            break;
          }
        }

        char[] digits = matches[off];
        digits[0] = d1;

        for (int k = 1; k < digits.length; ++k) {
          char match = digits[k];
          if (match == d2) {
            break;
          }
          if (match == 0) {
            digits[k] = d2;
            counts[off]++;
            break;
          }
        }

        for (int k = 1; k < digits.length; ++k) {
          char match = digits[k];
          if (match == d3) {
            break;
          }
          if (match == 0) {
            digits[k] = d3;
            counts[off]++;
            break;
          }
        }
      }

      // We've now saved all the digits. If any initial
      // character has 3 or more different digits that follow
      // it, it must be 1.

      int oneOffset = counts[0] >= 3 ? 0 : counts[1] >= 3 ? 1 : -1;

      // In the unlikely event that no initial digit has more
      // than 2 digits that follow it, we can deduce nothing.
      if (oneOffset == -1) {
        return;
      }

      this.mapCharacter(matches[oneOffset][0], 1);

      // If we have identified 1, if we have another initial
      // digit, it must be 2.

      int twoOffset = 1 - oneOffset;
      if (counts[twoOffset] == 0) {
        return;
      }

      this.mapCharacter(matches[twoOffset][0], 2);

      // 2 is always followed by 0
      this.mapCharacter(matches[twoOffset][1], 0);
    }

    // Step 2: Deduce digits from the dice game

    private final ArrayList<String> rolls = new ArrayList<>();

    public void addRoll(final String roll) {
      // AB-CD=xx
      if (roll.length() != 8) {
        return;
      }

      // See if it's a new roll
      for (int i = 0; i < this.rolls.size(); ++i) {
        String old = this.rolls.get(i);
        if (old.equals(roll)) {
          return;
        }
      }

      // We can work even without the laminated items if we
      // deduce digits from the die rolls.
      this.addNewDigit(roll.charAt(0));
      this.addNewDigit(roll.charAt(1));
      this.addNewDigit(roll.charAt(3));
      this.addNewDigit(roll.charAt(4));

      // Add the new roll to the list
      this.rolls.add(roll);
    }

    private int rollCount = 0;

    public void analyzeRolls() {
      // If we've already looked at all the rolls in the
      // array, it's pointless to do it again.
      if (this.rolls.size() == this.rollCount) {
        return;
      }

      // Save current size of array
      this.rollCount = this.rolls.size();

      // Match rolls against set of all possible combinations
      // and eliminate any which are inconsistent
      this.matchDigitPermutations();
    }

    private final Set<String> permutations = new HashSet<>();

    private void matchDigitPermutations() {
      // Nothing to do if we have identified all the digits
      if (this.digitMap.size() == 7) {
        return;
      }

      // We can't do this unless we know all 7 runes that are
      // used for digits.
      if (this.digits.size() != 7) {
        return;
      }

      // Initialize the set with 5040 permutations of 7 runes.
      if (this.permutations.size() == 0) {
        this.generatePermutations("");
        RequestLogger.printLine(
            "Initialized "
                + KoLConstants.COMMA_FORMAT.format(this.permutations.size())
                + " permutations");
      }

      // Iterate over all the rolls and eliminate any
      // permutation which is inconsistent
      int old_perms = this.permutations.size();
      for (int i = 0; i < this.rolls.size() && this.permutations.size() > 1; ++i) {
        this.checkPermutations(this.rolls.get(i));
        int new_perms = this.permutations.size();

        if (old_perms > new_perms) {
          int gone = old_perms - new_perms;
          RequestLogger.printLine(
              "Eliminated "
                  + KoLConstants.COMMA_FORMAT.format(gone)
                  + " permutations; "
                  + KoLConstants.COMMA_FORMAT.format(new_perms)
                  + " left");
          old_perms = new_perms;
        }
      }

      // If only a single permutation remains in the set, we
      // have cracked the digit code.
      if (this.permutations.size() == 1) {
        this.saveSoloPermutation();
      }
    }

    private void generatePermutations(final String prefix) {
      int index = prefix.length();

      if (index == 7) {
        this.permutations.add(prefix);
        return;
      }

      // If we know the character that goes in this position,
      // generate only the permutations that have that
      // character in that position.
      Character val = this.charMap.get(IntegerPool.get(index));
      if (val != null) {
        this.generatePermutations(prefix + val.charValue());
        return;
      }

      for (int i = 0; i < 7; ++i) {
        Character rune = this.digits.get(i);

        // If we're already using this character, skip
        char ch = rune.charValue();
        if (prefix.indexOf(ch) != -1) {
          continue;
        }

        // If we know this rune, only use it in the
        // correct position.
        Integer j = this.digitMap.get(rune);
        if (j != null && j.intValue() != index) {
          continue;
        }

        // Otherwise, put this character into position
        // and recurse.
        this.generatePermutations(prefix + ch);
      }
    }

    private void checkPermutations(final String roll) {
      // AB-CD=xx
      if (roll.length() != 8) {
        return;
      }

      char d1 = roll.charAt(0);
      char d2 = roll.charAt(1);
      char d3 = roll.charAt(3);
      char d4 = roll.charAt(4);
      int high = roll.charAt(6) - '0';
      int low = roll.charAt(7) - '0';
      int val = (high * 7) + low;

      Iterator<String> it = this.permutations.iterator();
      while (it.hasNext()) {
        String permutation = it.next();
        if (!this.validPermutation(permutation, d1, d2, d3, d4, val)) {
          it.remove();
        }
      }
    }

    private boolean validPermutation(
        final String permutation,
        final char d1,
        final char d2,
        final char d3,
        final char d4,
        final int val) {
      int total;

      int i1 = permutation.indexOf(d1);
      if (d1 == d2 && i1 == 0) {
        total = 49;
      } else {
        int i2 = permutation.indexOf(d2);
        total = i1 * 7 + i2;
      }

      int i3 = permutation.indexOf(d3);
      int i4 = permutation.indexOf(d4);
      total -= i3 * 7 + i4;

      return total == val;
    }

    private void saveSoloPermutation() {
      if (this.permutations.size() != 1) {
        return;
      }

      String[] strings = this.permutations.toArray(new String[1]);
      String digits = strings[0];
      for (int i = 0; i < 7; ++i) {
        this.mapCharacter(digits.charAt(i), i);
      }
    }

    /*
     * Start of obsolete code

    private void deduceLastDigit()
    {
      // Only do this if we know all but one of the digits
      // and all the characters which are used as digits.
      if ( this.digitMap.size() != 6 || this.digits.size() != 7 )
      {
        return;
      }

      // Find the character we have not used for a digit
      Character code = null;
      for ( int i = 0; i < 7; ++i )
      {
        Character c = (Character)this.digits.get( i );
        if ( !this.digitMap.containsKey( c ) )
        {
          code = c;
          break;
        }
      }

      // Find the digit we have not identified
      for ( int i = 0; i < 7; ++i )
      {
        Integer val = PrimitiveAutoBoxCache.valueOf( i );
        if ( this.charMap.get( val ) == null )
        {
          this.mapCharacter( code, val );
          return;
        }
      }
    }

    // The following technique is no longer used; solving via
    // permutation elimination allows us to all rolls, including
    // doubles, even if the digit for 0 is not yet known.

    private LinearSystem makeSystem()
    {
      // Iterate until we stop learning variables by simple
      // substitution.
      while ( true )
      {
        // The digit for zero is special: a roll of
        // "00" equals 49
        Character z = (Character)this.charMap.get( PrimitiveAutoBoxCache.valueOf( 0 ) );
        LinearSystem system = this.makeSystem( z );
        if ( system != null )
        {
          return system;
        }
      }
    }

    private LinearSystem makeSystem( final Character zero)
    {
      // Make a list of variables
      ArrayList variables = new ArrayList();
      for ( int i = 0; i < this.digits.size(); ++i )
      {
        Character c = (Character)this.digits.get( i );
        if ( !this.digitMap.containsKey( c ) )
        {
          variables.add( c );
        }
      }

      // Make a linear system containing these variables
      LinearSystem system = new LinearSystem( variables );

      // Give it equations
      for ( int i = 0; i < this.rolls.size(); ++i )
      {
        String roll = (String) this.rolls.get( i );

        // AB-CD=xx
        if ( roll.length() != 8 )
        {
          continue;
        }

        char d1 = roll.charAt( 0 );
        char d2 = roll.charAt( 1 );
        char d3 = roll.charAt( 3 );
        char d4 = roll.charAt( 4 );
        int high = roll.charAt( 6 ) - '0';
        int low = roll.charAt( 7 ) - '0';
        int val = ( high * 7) + low;

        // Double zero = 49, not 0. Unless we know what
        // zero is, we can't make that conversion, so
        // skip all doubles until we learn 0.

        if ( d1 == d2 )
        {
          if ( !this.digitMap.containsKey( new Character( d1 ) ) && zero == null )
          {
            continue;
          }

          // The linear equation solver doesn't
          // know that 00 is 49. Adjust sum.
          if ( d1 == zero.charValue() )
          {
            val -= 49;
          }
        }

        int known = this.digitMap.size();
        system.addEquation( 7, d1, 1, d2, -7, d3, -1, d4, val );

        // If adding an equation found a variable by
        // substitution, punt, since that could make
        // already added equations simpler.

        if ( known != this.digitMap.size() )
        {
          return null;
        }
      }

      return system;
    }

    private class LinearSystem
    {
      private final int vcount;
      private final Character[] variables;
      private final ArrayList equations;

      public LinearSystem( final ArrayList variables )
      {
        this.vcount = variables.size();
        this.variables = (Character[]) variables.toArray( new Character[ this.vcount ] );
        this.equations = new ArrayList();
      }

      public void addEquation( final int c1, final char v1,
             final int c2, final char v2,
             final int c3, final char v3,
             final int c4, final char v4,
             final int val )
      {
        int[] equation = new int[ this.vcount + 1];

        // Store the sum first, since we will adjust it.
        equation[ this.vcount ] = val;

        // Store or substitute all the variables
        this.addVariable( equation, c1, v1 );
        this.addVariable( equation, c2, v2 );
        this.addVariable( equation, c3, v3 );
        this.addVariable( equation, c4, v4 );

        // If we only have one variable unsubstituted
        // in this equation, we've discovered a new
        // digit.

        int offset = -1;
        for ( int i = 0; i < this.vcount; ++i )
        {
          // Skip unused variables
          if ( equation[i] == 0 )
          {
            continue;
          }

          // At least two unknown variables.
          // Register equation.
          if ( offset != -1 )
          {
            this.equations.add( equation );
            return;
          }

          // This variable is unknown
          offset = i;
        }

        // If there is only one unsubstituted variable,
        // register the new digit.
        if ( offset != -1 )
        {
          char code = this.variables[ offset ].charValue();
          int i = equation[ this.vcount ] / equation [ offset ];

          DwarfNumberTranslator.this.mapCharacter( code, i );
        }

        // All the variables in this equation are
        // already known. Ignore it.
      }

      public void addVariable( final int[] equation, final int c1, final char v1 )
      {
        Integer digit = DwarfNumberTranslator.this.getDigit( v1 );
        // If this is a known variable, substitute
        if ( digit != null )
        {
          int val = c1 * digit.intValue();
          equation[ this.vcount ] -= val;
          return;
        }

        // This is an unknown variable. Find it in the
        // list.

        for ( int i = 0; i < this.vcount; ++i )
        {
          if ( v1 == this.variables[ i ].charValue() )
          {
            equation[ i ] += c1;
            return;
          }
        }
      }

      private int [][] eqs = null;

      public void calculate()
      {
        int rows = this.equations.size();
        int cols = this.vcount;
        if ( rows == 0 || cols == 0 )
        {
          return;
        }

        // Make temporary array out of ArrayList
        this.eqs = (int[][])this.equations.toArray( new int[rows][cols+1]);

        // Start examining matrix at upper left corner
        int row = 0;
        int col = 0;

        // Iterate over the columns, going down the rows
        while ( col < cols )
        {
          // Find row which contains this variable
          int newRow = this.findRow( row, col );

          // If we found one, work with it.
          if ( newRow != -1 )
          {
            this.swapRows( row, newRow );

            // Eliminate this variable in
            // all rows below this one.
            this.eliminate( row, col );

            // Move to next row
            row++;
          }

          // Move to next column
          col++;
        }

        // Go back up the rows, substituting variables
        while ( --row >= 0 )
        {
          this.substitute( row );
        }

        // Clear temporary array
        this.eqs = null;
      }

      private int findRow( int row, final int col )
      {
        int nrows = this.eqs.length;
        while ( row < nrows )
        {
          if ( this.eqs[ row ][ col ] != 0 )
          {
            return row;
          }
          row++;
        }
        return -1;
      }

      private void swapRows( final int row1, final int row2 )
      {
        if ( row1 != row2 )
        {
          int[] temp = this.eqs[ row1 ];
          this.eqs[ row1 ] = this.eqs[ row2 ];
          this.eqs[ row2 ] = temp;
        }
      }

      private void eliminate( final int row, final int col )
      {
        // We will subtract row from all lower rows.
        // Its value for the specified column is a
        // constant we will use frequently.
        int[] row1 = this.eqs[ row ];
        int c1 = row1[ col ];

        int nrows = this.eqs.length;
        int ncols = this.vcount + 1;
        int current = row + 1;

        // Iterate over all rows below the specified row
        while ( current < nrows )
        {
          int[] row2 = this.eqs[ current++ ];
          int c2 = row2[ col ];
          if ( c2 == 0 )
          {
            continue;
          }

          // row2 = ( c1 * row2 ) - (c2 * row1)
          for ( int i = col; i < ncols; ++i )
          {
            row2[i] = c1 * row2[i] - c2 * row1[i] ;
          }
        }
      }

      private void substitute( final int row )
      {
        int[] row1 = this.eqs[ row ];

        // Iterate over the variables
        int ncols = this.vcount;
        int index = -1;
        boolean valid = true;

        for ( int col = 0; col < ncols; ++col )
        {
          int c1 = row1[col];
          if ( c1 == 0 )
          {
            continue;
          }

          Integer digit = DwarfNumberTranslator.this.getDigit( this.variables[col] );
          // If this variable is known, substitute
          if ( digit != null )
          {
            int val = c1 * digit.intValue();

            row1[col] = 0;
            row1[ ncols ] -= val;
            continue;
          }

          // If this variable is unknown, it's a
          // candidate for discover.

          if ( index != -1 )
          {
            // Oops. More than one unknown
            valid = false;
          }

          // Save index of unknown variable
          index = col;
        }

        // If we discovered a single unknown variable,
        // we can now solve for it.
        if ( valid && index != -1 )
        {
          Character digit = this.variables[index];
          Integer val = PrimitiveAutoBoxCache.valueOf( row1[ncols] / row1[index] );
          DwarfNumberTranslator.this.mapCharacter( digit, val );
        }
      }
    }

    * End of obsolete code
    */
  }

  public static class FactoryData {
    public static final int HAT = 0;
    public static final int PANTS = 1;
    public static final int WEAPON = 2;

    private final DwarfNumberTranslator digits;
    private final Map<Character, Integer> itemMap = new HashMap<>();
    private final Map<Integer, Character> runeMap = new HashMap<>();

    // Indexed by [item]
    private final char[] equipment = new char[3];

    // Indexed by [hopper]
    private final int[] ores = new int[4];

    // Indexed by [item][hopper]
    private final int[][] oreQuantities = new int[3][4];
    private final int[][] gaugeSettings = new int[3][4];

    public FactoryData(final DwarfNumberTranslator digits) {
      // Get a Dwarf Number Translator
      this.digits = digits;

      // Make maps from dwarf word rune to itemId and vice versa
      for (int i = 0; i < DwarfFactoryRequest.ITEMS.length; ++i) {
        int itemId = DwarfFactoryRequest.ITEMS[i];
        String setting = "lastDwarfFactoryItem" + itemId;
        String value = Preferences.getString(setting);
        if (value.length() == 1) {
          Character rune = Character.valueOf(value.charAt(0));
          Integer id = IntegerPool.get(itemId);
          this.itemMap.put(rune, id);
          this.runeMap.put(id, rune);
        }
      }

      // Get the 3 pieces of equipment
      this.equipment[HAT] = this.findRune(ItemPool.MINERS_HELMET);
      this.equipment[PANTS] = this.findRune(ItemPool.MINERS_PANTS);
      this.equipment[WEAPON] = this.findRune(ItemPool.MATTOCK);

      // Get the 4 ores in hopper order
      this.ores[0] = this.findItem(Preferences.getString("lastDwarfHopper1"));
      this.ores[1] = this.findItem(Preferences.getString("lastDwarfHopper2"));
      this.ores[2] = this.findItem(Preferences.getString("lastDwarfHopper3"));
      this.ores[3] = this.findItem(Preferences.getString("lastDwarfHopper4"));

      // Translate the unlaminated items into ore quantities
      this.getOreQuantities();

      // Translate the laminated items into gauge settings
      this.getGaugeSettings();
    }

    public String getHopperOre(final int hopper) {
      if (hopper < 0 || hopper > 3) {
        return "";
      }
      return ItemDatabase.getItemName(this.ores[hopper]);
    }

    public void setInventoryCounts() {
      // Get the current count of ore in inventory
      for (int i = 0; i < 4; ++i) {
        int itemId = this.ores[i];
        if (itemId != -1) {
          DwarfFactoryRequest.inventoryCount[i] = InventoryManager.getCount(itemId);
        }
      }
    }

    public String getHopperOreCounts(final int hopper) {
      if (hopper < 0 || hopper > 3) {
        return "";
      }
      String hcount = String.valueOf(DwarfFactoryRequest.hopperCount[hopper]);
      String icount = String.valueOf(DwarfFactoryRequest.inventoryCount[hopper]);
      return hcount + " (H)/" + icount + " (I)";
    }

    public int getOreOnHand(final int hopper) {
      int hcount = DwarfFactoryRequest.hopperCount[hopper];
      int icount = DwarfFactoryRequest.inventoryCount[hopper];
      return hcount + icount;
    }

    public AdventureResult getOre(final int item, final int hopper) {
      if (item < 0 || item > 2 || hopper < 0 || hopper > 3) {
        return null;
      }
      int itemId = this.ores[hopper];
      if (itemId == -1) {
        return null;
      }
      int count = this.oreQuantities[item][hopper];
      return ItemPool.get(itemId, count);
    }

    public int getOreQuantity(final int item, final int hopper) {
      if (item < 0 || item > 2 || hopper < 0 || hopper > 3) {
        return 0;
      }
      return this.oreQuantities[item][hopper];
    }

    public String getOreQuantityString(final int item, final int hopper) {
      int needed = this.getOreQuantity(item, hopper);
      int have = this.getOreOnHand(hopper);
      String color = (have >= needed) ? "green" : "red";
      return "<font color=" + color + ">" + needed + "</font>";
    }

    private void setOreQuantity(final int item, final int hopper, final int value) {
      this.oreQuantities[item][hopper] = value;
    }

    public int getGaugeSetting(final int item, final int hopper) {
      if (item < 0 || item > 2 || hopper < 0 || hopper > 3) {
        return -1;
      }
      return this.gaugeSettings[item][hopper];
    }

    private void setGaugeSetting(final int item, final int hopper, final int value) {
      this.gaugeSettings[item][hopper] = value;
    }

    private int findItem(final String rune) {
      if (rune.length() != 1) {
        return -1;
      }
      return this.findItem(rune.charAt(0));
    }

    private int findItem(final char rune) {
      Integer val = this.itemMap.get(Character.valueOf(rune));
      return val == null ? -1 : val.intValue();
    }

    private char findRune(final int itemId) {
      Character val = this.runeMap.get(IntegerPool.get(itemId));
      return val == null ? 0 : val.charValue();
    }

    private int findHopper(final char rune) {
      int item = this.findItem(rune);

      if (item == -1) {
        return -1;
      }

      for (int i = 0; i < 4; ++i) {
        if (item == this.ores[i]) {
          return i;
        }
      }

      return -1;
    }

    private int findEquipment(final char rune) {
      for (int i = 0; i < 3; ++i) {
        if (rune == this.equipment[i]) {
          return i;
        }
      }
      return -1;
    }

    private void getGaugeSettings() {
      this.getGaugeSetting(Preferences.getString("lastDwarfOfficeItem3208"));
      this.getGaugeSetting(Preferences.getString("lastDwarfOfficeItem3209"));
      this.getGaugeSetting(Preferences.getString("lastDwarfOfficeItem3210"));
      this.getGaugeSetting(Preferences.getString("lastDwarfOfficeItem3211"));
    }

    private void getGaugeSetting(final String setting) {
      // lastDwarfOfficeItem3208=B,HGIG,MGDE,PJD
      // lastDwarfOfficeItem3209=O,HGAA,MGAG,PGEA
      // lastDwarfOfficeItem3210=J,HFJ,MGED,PGAG
      // lastDwarfOfficeItem3211=Q,HGE,MGGI,PGG

      String[] splits = setting.split(",");
      if (splits.length != 4) {
        return;
      }

      int hopper = this.findHopper(splits[0].charAt(0));
      if (hopper < 0) {
        return;
      }

      for (int i = 1; i <= 3; ++i) {
        int item = this.findEquipment(splits[i].charAt(0));
        if (item < 0) {
          continue;
        }

        int number = this.digits.parseNumber(splits[i].substring(1));
        this.setGaugeSetting(item, hopper, number);
      }
    }

    private void getOreQuantities() {
      this.getOreQuantity(Preferences.getString("lastDwarfOfficeItem3212"));
      this.getOreQuantity(Preferences.getString("lastDwarfOfficeItem3213"));
      this.getOreQuantity(Preferences.getString("lastDwarfOfficeItem3214"));
    }

    private void getOreQuantity(final String setting) {
      // lastDwarfOfficeItem3212=H,QEG,BFD,OJI,JED
      // lastDwarfOfficeItem3213=M,OGD,BGD,QJI,JGJ
      // lastDwarfOfficeItem3214=P,JFJ,OJE,BGA,QEG

      String[] splits = setting.split(",");
      if (splits.length != 5) {
        return;
      }

      int item = this.findEquipment(splits[0].charAt(0));
      if (item < 0) {
        return;
      }

      for (int i = 1; i <= 4; ++i) {
        int hopper = this.findHopper(splits[i].charAt(0));
        if (hopper < 0) {
          continue;
        }

        int number = this.digits.parseNumber(splits[i].substring(1));
        this.setOreQuantity(item, hopper, number);
      }
    }
  }
}
