class TextInstructions {

  public String compile(LegStep step) {
    if (step.getManeuver() == null) {
      throw new RuntimeException("No step maneuver provided.");
    }

    String type = step.getManeuver().getType();
    String modifier = step.getManeuver().getModifier();
    String mode = step.getMode();

    if (TextUtils.isEmpty(type)) {
      throw new RuntimeException("Missing step maneuver type.");
    }

    if (!type.equals("depart") && !type.equals("arrive") && TextUtils.isEmpty(modifier)) {
      throw new RuntimeException("Missing step maneuver modifier.");
    }

    if (getVersionObject().getAsJsonObject(type) == null) {
      // Log for debugging
      logger.log(Level.FINE, "Encountered unknown instruction type: " + type);

      // OSRM specification assumes turn types can be added without
      // major version changes. Unknown types are to be treated as
      // type `turn` by clients
      type = "turn";
    }

    // Use special instructions if available, otherwise `defaultinstruction`
    JsonObject instructionObject;
    JsonObject modeValue = getVersionObject().getAsJsonObject("modes").getAsJsonObject(mode);
    if (modeValue != null) {
      instructionObject = modeValue;
    } else {
      JsonObject modifierValue = getVersionObject().getAsJsonObject(type).getAsJsonObject(modifier);
      instructionObject = modifierValue == null
        ? getVersionObject().getAsJsonObject(type).getAsJsonObject("default")
        : modifierValue;
    }

    // Special case handling
    JsonPrimitive laneInstruction = null;
    switch (type) {
      case "use lane":
        laneInstruction = getVersionObject().getAsJsonObject("constants")
          .getAsJsonObject("lanes").getAsJsonPrimitive(laneConfig(step));
        if (laneInstruction == null) {
          // If the lane combination is not found, default to continue straight
          instructionObject = getVersionObject().getAsJsonObject("use lane")
            .getAsJsonObject("no_lanes");
        }
        break;
      case "rotary":
      case "roundabout":
        if (!TextUtils.isEmpty(step.getRotaryName())
          && step.getManeuver().getExit() != null
          && instructionObject.getAsJsonObject("name_exit") != null) {
          instructionObject = instructionObject.getAsJsonObject("name_exit");
        } else if (step.getRotaryName() != null && instructionObject.getAsJsonObject("name") != null) {
          instructionObject = instructionObject.getAsJsonObject("name");
        } else if (step.getManeuver().getExit() != null && instructionObject.getAsJsonObject("exit") != null) {
          instructionObject = instructionObject.getAsJsonObject("exit");
        } else {
          instructionObject = instructionObject.getAsJsonObject("default");
        }
        break;
      default:
        // NOOP, since no special logic for that type
    }

    // Decide way_name with special handling for name and ref
    String wayName;
    String name = TextUtils.isEmpty(step.getName()) ? "" : step.getName();
    String ref = TextUtils.isEmpty(step.getRef()) ? "" : step.getRef().split(";")[0];

    // Remove hacks from Mapbox Directions mixing ref into name
    if (name.equals(step.getRef())) {
      // if both are the same we assume that there used to be an empty name, with the ref being filled in for it
      // we only need to retain the ref then
      name = "";
    }
    name = name.replace(" (" + step.getRef() + ")", "");

    if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(ref) && !name.equals(ref)) {
      wayName = name + " (" + ref + ")";
    } else if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(ref)) {
      wayName = ref;
    } else {
      wayName = name;
    }

    // Decide which instruction string to use
    // Destination takes precedence over name
    String instruction;
    if (!TextUtils.isEmpty(step.getDestinations())
      && instructionObject.getAsJsonPrimitive("destination") != null) {
      instruction = instructionObject.getAsJsonPrimitive("destination").getAsString();
    } else if (!TextUtils.isEmpty(wayName)
      && instructionObject.getAsJsonPrimitive("name") != null) {
      instruction = instructionObject.getAsJsonPrimitive("name").getAsString();
    } else {
      instruction = instructionObject.getAsJsonPrimitive("default").getAsString();
    }

    if (getTokenizedInstructionHook() != null) {
      instruction = getTokenizedInstructionHook().change(instruction);
    }

    // Replace tokens
    // NOOP if they don't exist
    String nthWaypoint = ""; // TODO, add correct waypoint counting
    JsonPrimitive modifierValue =
      getVersionObject().getAsJsonObject("constants").getAsJsonObject("modifier").getAsJsonPrimitive(modifier);
    instruction = instruction
      .replace("{way_name}", wayName)
      .replace("{destination}", TextUtils.isEmpty(step.getDestinations()) ? "" : step.getDestinations().split(",")[0])
      .replace("{exit_number}",
        step.getManeuver().getExit() == null ? ordinalize(1) : ordinalize(step.getManeuver().getExit()))
      .replace("{rotary_name}", TextUtils.isEmpty(step.getRotaryName()) ? "" : step.getRotaryName())
      .replace("{lane_instruction}", laneInstruction == null ? "" : laneInstruction.getAsString())
      .replace("{modifier}", modifierValue == null ? "" : modifierValue.getAsString())
      .replace("{direction}", directionFromDegree(step.getManeuver().getBearingAfter()))
      .replace("{nth}", nthWaypoint)
      .replaceAll("\\s+", " "); // remove excess spaces

    if (getRootObject().getAsJsonObject("meta").getAsJsonPrimitive("capitalizeFirstLetter").getAsBoolean()) {
      instruction = capitalizeFirstLetter(instruction);
    }

    return instruction;
  }

}

class TextInstructionsTest  {

  @Rule
  public ExpectedException thrown;

  @Test
  public void testFixturesMatchGeneratedInstructions() throws IOException {

    for (String fixture : TextInstructionsFixtures.FIXTURES) {
      String body = loadJsonFixture(fixture);
      FixtureModel model = new Gson().fromJson(body, FixtureModel.class);
      for (Object entry : model.getInstructions().entrySet()) {
        Map.Entry pair = (Map.Entry) entry;
        String language = (String) pair.getKey();
        String compiled = (String) pair.getValue();
        assertEquals(compiled, new TextInstructions(language, "v5").compile(model.getStep()));
      }
    }
  }

}