/**
   * Return the next flight event to handle, or null if no more events should be handled. This
   * method jumps the simulation time forward in case no motors have been ignited. The flight event
   * is removed from the event queue.
   *
   * @return the flight event to handle, or null
   */
  private FlightEvent nextEvent() {
    EventQueue queue = currentStatus.getEventQueue();
    FlightEvent event = queue.peek();
    if (event == null) return null;

    // Jump to event if no motors have been ignited
    if (!currentStatus.isMotorIgnited() && event.getTime() > currentStatus.getSimulationTime()) {
      currentStatus.setSimulationTime(event.getTime());
    }
    if (event.getTime() <= currentStatus.getSimulationTime()) {
      return queue.poll();
    } else {
      return null;
    }
  }
  /**
   * Handles events occurring during the flight from the event queue. Each event that has occurred
   * before or at the current simulation time is processed. Suitable events are also added to the
   * flight data.
   */
  private boolean handleEvents() throws SimulationException {
    boolean ret = true;
    FlightEvent event;

    log.trace("HandleEvents: current branch = " + currentStatus.getFlightData().getBranchName());
    log.trace("EventQueue = " + currentStatus.getEventQueue().toString());
    for (event = nextEvent(); event != null; event = nextEvent()) {

      // Ignore events for components that are no longer attached to the rocket
      if (event.getSource() != null
          && event.getSource().getParent() != null
          && !currentStatus.getConfiguration().isComponentActive(event.getSource())) {
        continue;
      }

      // Call simulation listeners, allow aborting event handling
      if (!SimulationListenerHelper.fireHandleFlightEvent(currentStatus, event)) {
        continue;
      }

      if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
        RecoveryDevice device = (RecoveryDevice) event.getSource();
        if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(currentStatus, device)) {
          continue;
        }
      }

      // Check for motor ignition events, add ignition events to queue
      for (MotorClusterState state : currentStatus.getActiveMotors()) {
        if (state.testForIgnition(event)) {
          final double simulationTime = currentStatus.getSimulationTime();
          MotorClusterState sourceState = (MotorClusterState) event.getData();
          double ignitionDelay = 0;
          if ((event.getType() == FlightEvent.Type.BURNOUT)
              || (event.getType() == FlightEvent.Type.EJECTION_CHARGE)) {
            ignitionDelay = sourceState.getEjectionDelay();
          }
          final double ignitionTime = currentStatus.getSimulationTime() + ignitionDelay;
          final RocketComponent mount = (RocketComponent) state.getMount();

          // TODO:  this event seems to get enqueue'd multiple times ...
          log.info("Queueing Ignition Event for: " + state.toDescription() + " @: " + ignitionTime);
          // log.info("     Because of "+event.getType().name()+" @"+event.getTime()+" from:
          // "+event.getSource().getName());

          addEvent(new FlightEvent(FlightEvent.Type.IGNITION, ignitionTime, mount, state));
        }
      }

      // Check for stage separation event
      for (AxialStage stage : currentStatus.getConfiguration().getActiveStages()) {
        int stageNo = stage.getStageNumber();
        if (stageNo == 0) continue;

        StageSeparationConfiguration separationConfig =
            stage.getSeparationConfigurations().get(this.fcid);
        if (separationConfig.getSeparationEvent().isSeparationEvent(event, stage)) {
          addEvent(
              new FlightEvent(
                  FlightEvent.Type.STAGE_SEPARATION,
                  event.getTime() + separationConfig.getSeparationDelay(),
                  stage));
        }
      }

      // Check for recovery device deployment, add events to queue
      for (RocketComponent c : currentStatus.getConfiguration().getActiveComponents()) {
        if (!(c instanceof RecoveryDevice)) continue;
        DeploymentConfiguration deployConfig =
            ((RecoveryDevice) c).getDeploymentConfigurations().get(this.fcid);
        if (deployConfig.isActivationEvent(event, c)) {
          // Delay event by at least 1ms to allow stage separation to occur first
          addEvent(
              new FlightEvent(
                  FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
                  event.getTime() + Math.max(0.001, deployConfig.getDeployDelay()),
                  c));
        }
      }

      // Handle event
      switch (event.getType()) {
        case LAUNCH:
          {
            currentStatus.getFlightData().addEvent(event);
            break;
          }

        case IGNITION:
          {
            MotorClusterState motorState = (MotorClusterState) event.getData();

            log.info(
                "  Igniting motor: "
                    + motorState.toDescription()
                    + " @"
                    + currentStatus.getSimulationTime());
            motorState.ignite(event.getTime());

            // Ignite the motor
            currentStatus.setMotorIgnited(true);
            currentStatus.getFlightData().addEvent(event);

            // ... ignite ...uhh, again?
            // TBH, I'm not sure what this call is for. It seems to be mostly a bunch of event
            // distribution.
            MotorConfigurationId motorId = motorState.getID();
            MotorMount mount = (MotorMount) event.getSource();
            if (!SimulationListenerHelper.fireMotorIgnition(
                currentStatus, motorId, mount, motorState)) {
              continue;
            }

            // and queue up the burnout for this motor, as well.
            double duration = motorState.getMotor().getBurnTimeEstimate();
            double burnout = currentStatus.getSimulationTime() + duration;
            addEvent(
                new FlightEvent(FlightEvent.Type.BURNOUT, burnout, event.getSource(), motorState));
            break;
          }

        case LIFTOFF:
          {
            // Mark lift-off as occurred
            currentStatus.setLiftoff(true);
            currentStatus.getFlightData().addEvent(event);
            break;
          }

        case LAUNCHROD:
          {
            // Mark launch rod as cleared
            currentStatus.setLaunchRodCleared(true);
            currentStatus.getFlightData().addEvent(event);
            break;
          }

        case BURNOUT:
          {
            // If motor burnout occurs without lift-off, abort
            if (!currentStatus.isLiftoff()) {
              throw new SimulationLaunchException(
                  trans.get("BasicEventSimulationEngine.error.earlyMotorBurnout"));
            }

            // Add ejection charge event
            MotorClusterState motorState = (MotorClusterState) event.getData();
            motorState.burnOut(event.getTime());

            AxialStage stage = motorState.getMount().getStage();
            log.debug(
                " adding EJECTION_CHARGE event for stage "
                    + stage.getStageNumber()
                    + ": "
                    + stage.getName());
            log.debug(
                "                         .... for motor "
                    + motorState.getMotor().getDesignation());

            double delay = motorState.getEjectionDelay();
            if (motorState.hasEjectionCharge()) {
              addEvent(
                  new FlightEvent(
                      FlightEvent.Type.EJECTION_CHARGE,
                      currentStatus.getSimulationTime() + delay,
                      stage,
                      event.getData()));
            }
            currentStatus.getFlightData().addEvent(event);
            break;
          }

        case EJECTION_CHARGE:
          {
            MotorClusterState motorState = (MotorClusterState) event.getData();
            motorState.expend(event.getTime());
            currentStatus.getFlightData().addEvent(event);
            break;
          }

        case STAGE_SEPARATION:
          {
            // Record the event.
            currentStatus.getFlightData().addEvent(event);

            RocketComponent boosterStage = event.getSource();
            final int stageNumber = boosterStage.getStageNumber();

            // Mark the status as having dropped the booster
            currentStatus.getConfiguration().clearStage(stageNumber);

            // Prepare the simulation branch
            SimulationStatus boosterStatus = new SimulationStatus(currentStatus);
            boosterStatus.setFlightData(
                new FlightDataBranch(boosterStage.getName(), FlightDataType.TYPE_TIME));
            // Mark the booster status as only having the booster.
            boosterStatus.getConfiguration().setOnlyStage(stageNumber);
            toSimulate.add(boosterStatus);
            log.info(
                String.format(
                    "==>> @ %g; from Branch: %s ---- Branching: %s ---- \n",
                    currentStatus.getSimulationTime(),
                    currentStatus.getFlightData().getBranchName(),
                    boosterStatus.getFlightData().getBranchName()));

            break;
          }

        case APOGEE:
          // Mark apogee as reached
          currentStatus.setApogeeReached(true);
          currentStatus.getFlightData().addEvent(event);
          // This apogee event might be the optimum if recovery has not already happened.
          if (currentStatus.getSimulationConditions().isCalculateExtras()
              && currentStatus.getDeployedRecoveryDevices().size() == 0) {
            currentStatus.getFlightData().setOptimumAltitude(currentStatus.getMaxAlt());
            currentStatus.getFlightData().setTimeToOptimumAltitude(currentStatus.getMaxAltTime());
          }
          break;

        case RECOVERY_DEVICE_DEPLOYMENT:
          RocketComponent c = event.getSource();
          int n = c.getStageNumber();
          // Ignore event if stage not active
          if (currentStatus.getConfiguration().isStageActive(n)) {
            // TODO: HIGH: Check stage activeness for other events as well?

            // Check whether any motor in the active stages is active anymore
            for (MotorClusterState state : currentStatus.getActiveMotors()) {
              if (state.isSpent()) {
                continue;
              }
              currentStatus.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
            }

            // Check for launch rod
            if (!currentStatus.isLaunchRodCleared()) {
              currentStatus.getWarnings().add(Warning.RECOVERY_LAUNCH_ROD);
            }

            // Check current velocity
            if (currentStatus.getRocketVelocity().length() > 20) {
              currentStatus
                  .getWarnings()
                  .add(new Warning.HighSpeedDeployment(currentStatus.getRocketVelocity().length()));
            }

            currentStatus.setLiftoff(true);
            currentStatus.getDeployedRecoveryDevices().add((RecoveryDevice) c);

            // If we haven't already reached apogee, then we need to compute the actual coast time
            // to determine the optimum altitude.
            if (currentStatus.getSimulationConditions().isCalculateExtras()
                && !currentStatus.isApogeeReached()) {
              FlightData coastStatus = computeCoastTime();

              currentStatus.getFlightData().setOptimumAltitude(coastStatus.getMaxAltitude());
              currentStatus.getFlightData().setTimeToOptimumAltitude(coastStatus.getTimeToApogee());
            }

            this.currentStepper = this.landingStepper;
            this.currentStatus = currentStepper.initialize(currentStatus);

            currentStatus.getFlightData().addEvent(event);
          }
          break;

        case GROUND_HIT:
          currentStatus.getFlightData().addEvent(event);
          break;

        case SIMULATION_END:
          ret = false;
          currentStatus.getFlightData().addEvent(event);
          break;

        case ALTITUDE:
          log.trace("BasicEventSimulationEngine:  Handling event " + event);
          break;

        case TUMBLE:
          this.currentStepper = this.tumbleStepper;
          this.currentStatus = currentStepper.initialize(currentStatus);
          currentStatus.getFlightData().addEvent(event);
          break;
      }
    }

    if (1200 < currentStatus.getSimulationTime()) {
      ret = false;
      log.error("Simulation hit max time (1200s): aborting.");
      currentStatus
          .getFlightData()
          .addEvent(
              new FlightEvent(FlightEvent.Type.SIMULATION_END, currentStatus.getSimulationTime()));
    }

    // If no motor has ignited, abort
    if (!currentStatus.isMotorIgnited()) {
      throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noIgnition"));
    }

    return ret;
  }
  @Override
  public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException {
    Set<MotorId> motorBurntOut = new HashSet<MotorId>();

    // Set up flight data
    FlightData flightData = new FlightData();

    // Set up rocket configuration
    Configuration configuration = setupConfiguration(simulationConditions);
    MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration);
    if (motorConfiguration.getMotorIDs().isEmpty()) {
      throw new MotorIgnitionException("No motors defined in the simulation.");
    }

    // Initialize the simulation
    currentStepper = flightStepper;
    status = initialStatus(configuration, motorConfiguration, simulationConditions, flightData);
    status = currentStepper.initialize(status);

    SimulationListenerHelper.fireStartSimulation(status);
    // Get originating position (in case listener has modified launch position)
    Coordinate origin = status.getRocketPosition();
    Coordinate originVelocity = status.getRocketVelocity();

    try {
      double maxAlt = Double.NEGATIVE_INFINITY;

      // Start the simulation
      while (handleEvents()) {

        // Take the step
        double oldAlt = status.getRocketPosition().z;

        if (SimulationListenerHelper.firePreStep(status)) {
          // Step at most to the next event
          double maxStepTime = Double.MAX_VALUE;
          FlightEvent nextEvent = status.getEventQueue().peek();
          if (nextEvent != null) {
            maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001);
          }
          log.verbose(
              "BasicEventSimulationEngine: Taking simulation step at t="
                  + status.getSimulationTime());
          currentStepper.step(status, maxStepTime);
        }
        SimulationListenerHelper.firePostStep(status);

        // Calculate values for custom expressions
        FlightDataBranch data = status.getFlightData();
        ArrayList<CustomExpression> allExpressions =
            status.getSimulationConditions().getSimulation().getCustomExpressions();
        for (CustomExpression expression : allExpressions) {
          data.setValue(expression.getType(), expression.evaluate(status));
        }

        // Check for NaN values in the simulation status
        checkNaN();

        // Add altitude event
        addEvent(
            new FlightEvent(
                FlightEvent.Type.ALTITUDE,
                status.getSimulationTime(),
                status.getConfiguration().getRocket(),
                new Pair<Double, Double>(oldAlt, status.getRocketPosition().z)));

        if (status.getRocketPosition().z > maxAlt) {
          maxAlt = status.getRocketPosition().z;
        }

        // Position relative to start location
        Coordinate relativePosition = status.getRocketPosition().sub(origin);

        // Add appropriate events
        if (!status.isLiftoff()) {

          // Avoid sinking into ground before liftoff
          if (relativePosition.z < 0) {
            status.setRocketPosition(origin);
            status.setRocketVelocity(originVelocity);
          }
          // Detect lift-off
          if (relativePosition.z > 0.02) {
            addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime()));
          }

        } else {

          // Check ground hit after liftoff
          if (status.getRocketPosition().z < 0) {
            status.setRocketPosition(status.getRocketPosition().setZ(0));
            addEvent(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.getSimulationTime()));
            addEvent(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime()));
          }
        }

        // Check for launch guide clearance
        if (!status.isLaunchRodCleared()
            && relativePosition.length() > status.getSimulationConditions().getLaunchRodLength()) {
          addEvent(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.getSimulationTime(), null));
        }

        // Check for apogee
        if (!status.isApogeeReached() && status.getRocketPosition().z < maxAlt - 0.01) {
          addEvent(
              new FlightEvent(
                  FlightEvent.Type.APOGEE,
                  status.getSimulationTime(),
                  status.getConfiguration().getRocket()));
        }

        // Check for burnt out motors
        for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
          MotorInstance motor = status.getMotorConfiguration().getMotorInstance(motorId);
          if (!motor.isActive() && motorBurntOut.add(motorId)) {
            addEvent(
                new FlightEvent(
                    FlightEvent.Type.BURNOUT,
                    status.getSimulationTime(),
                    (RocketComponent) status.getMotorConfiguration().getMotorMount(motorId),
                    motorId));
          }
        }
      }

    } catch (SimulationException e) {
      SimulationListenerHelper.fireEndSimulation(status, e);
      throw e;
    }

    SimulationListenerHelper.fireEndSimulation(status, null);

    flightData.addBranch(status.getFlightData());

    if (!flightData.getWarningSet().isEmpty()) {
      log.info("Warnings at the end of simulation:  " + flightData.getWarningSet());
    }

    // TODO: HIGH: Simulate branches
    return flightData;
  }
  private FlightDataBranch simulateLoop() {

    // Initialize the simulation
    currentStepper = flightStepper;
    currentStatus = currentStepper.initialize(currentStatus);

    // Get originating position (in case listener has modified launch position)
    Coordinate origin = currentStatus.getRocketPosition();
    Coordinate originVelocity = currentStatus.getRocketVelocity();

    try {

      // Start the simulation
      while (handleEvents()) {
        // Take the step
        double oldAlt = currentStatus.getRocketPosition().z;

        if (SimulationListenerHelper.firePreStep(currentStatus)) {
          // Step at most to the next event
          double maxStepTime = Double.MAX_VALUE;
          FlightEvent nextEvent = currentStatus.getEventQueue().peek();
          if (nextEvent != null) {
            maxStepTime =
                MathUtil.max(nextEvent.getTime() - currentStatus.getSimulationTime(), 0.001);
          }
          log.trace(
              "BasicEventSimulationEngine: Taking simulation step at t="
                  + currentStatus.getSimulationTime());
          currentStepper.step(currentStatus, maxStepTime);
        }
        SimulationListenerHelper.firePostStep(currentStatus);

        // Check for NaN values in the simulation status
        checkNaN();

        // Add altitude event
        addEvent(
            new FlightEvent(
                FlightEvent.Type.ALTITUDE,
                currentStatus.getSimulationTime(),
                currentStatus.getConfiguration().getRocket(),
                new Pair<Double, Double>(oldAlt, currentStatus.getRocketPosition().z)));

        if (currentStatus.getRocketPosition().z > currentStatus.getMaxAlt()) {
          currentStatus.setMaxAlt(currentStatus.getRocketPosition().z);
        }

        // Position relative to start location
        Coordinate relativePosition = currentStatus.getRocketPosition().sub(origin);

        // Add appropriate events
        if (!currentStatus.isLiftoff()) {

          // Avoid sinking into ground before liftoff
          if (relativePosition.z < 0) {
            currentStatus.setRocketPosition(origin);
            currentStatus.setRocketVelocity(originVelocity);
          }
          // Detect lift-off
          if (relativePosition.z > 0.02) {
            addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, currentStatus.getSimulationTime()));
          }

        } else {

          // Check ground hit after liftoff
          if (currentStatus.getRocketPosition().z < 0) {
            currentStatus.setRocketPosition(currentStatus.getRocketPosition().setZ(0));
            addEvent(
                new FlightEvent(FlightEvent.Type.GROUND_HIT, currentStatus.getSimulationTime()));
            addEvent(
                new FlightEvent(
                    FlightEvent.Type.SIMULATION_END, currentStatus.getSimulationTime()));
          }
        }

        // Check for launch guide clearance
        if (!currentStatus.isLaunchRodCleared()
            && relativePosition.length()
                > currentStatus.getSimulationConditions().getLaunchRodLength()) {
          addEvent(
              new FlightEvent(FlightEvent.Type.LAUNCHROD, currentStatus.getSimulationTime(), null));
        }

        // Check for apogee
        if (!currentStatus.isApogeeReached()
            && currentStatus.getRocketPosition().z < currentStatus.getMaxAlt() - 0.01) {
          currentStatus.setMaxAltTime(currentStatus.getSimulationTime());
          addEvent(
              new FlightEvent(
                  FlightEvent.Type.APOGEE,
                  currentStatus.getSimulationTime(),
                  currentStatus.getConfiguration().getRocket()));
        }

        //				//@Obsolete
        //				//@Redundant
        //				// Check for burnt out motors
        //				for( MotorClusterState state : currentStatus.getActiveMotors()){
        //					if ( state.isSpent()){
        //						addEvent(new FlightEvent(FlightEvent.Type.BURNOUT,
        // currentStatus.getSimulationTime(),
        //								(RocketComponent) state.getMount(), state));
        //					}
        //				}

        // Check for Tumbling
        // Conditions for transision are:
        //  apogee reached (if sustainer stage)
        // and is not already tumbling
        // and not stable (cg > cp)
        // and aoa > AOA_TUMBLE_CONDITION threshold
        // and thrust < THRUST_TUMBLE_CONDITION threshold

        if (!currentStatus.isTumbling()) {
          final double t = currentStatus.getFlightData().getLast(FlightDataType.TYPE_THRUST_FORCE);
          final double cp = currentStatus.getFlightData().getLast(FlightDataType.TYPE_CP_LOCATION);
          final double cg = currentStatus.getFlightData().getLast(FlightDataType.TYPE_CG_LOCATION);
          final double aoa = currentStatus.getFlightData().getLast(FlightDataType.TYPE_AOA);

          final boolean wantToTumble = (cg > cp && aoa > AOA_TUMBLE_CONDITION);

          if (wantToTumble) {
            final boolean tooMuchThrust = t > THRUST_TUMBLE_CONDITION;
            // final boolean isSustainer = status.getConfiguration().isStageActive(0);
            final boolean isApogee = currentStatus.isApogeeReached();
            if (tooMuchThrust) {
              currentStatus.getWarnings().add(Warning.TUMBLE_UNDER_THRUST);
            } else if (isApogee) {
              addEvent(new FlightEvent(FlightEvent.Type.TUMBLE, currentStatus.getSimulationTime()));
              currentStatus.setTumbling(true);
            }
          }
        }
      }

    } catch (SimulationException e) {
      SimulationListenerHelper.fireEndSimulation(currentStatus, e);
      // Add FlightEvent for Abort.
      currentStatus
          .getFlightData()
          .addEvent(
              new FlightEvent(
                  FlightEvent.Type.EXCEPTION,
                  currentStatus.getSimulationTime(),
                  currentStatus.getConfiguration().getRocket(),
                  e.getLocalizedMessage()));
      currentStatus.getWarnings().add(e.getLocalizedMessage());
    }

    return currentStatus.getFlightData();
  }
  /**
   * Handles events occurring during the flight from the event queue. Each event that has occurred
   * before or at the current simulation time is processed. Suitable events are also added to the
   * flight data.
   */
  private boolean handleEvents() throws SimulationException {
    boolean ret = true;
    FlightEvent event;

    for (event = nextEvent(); event != null; event = nextEvent()) {

      // Ignore events for components that are no longer attached to the rocket
      if (event.getSource() != null
          && event.getSource().getParent() != null
          && !status.getConfiguration().isStageActive(event.getSource().getStageNumber())) {
        continue;
      }

      // Call simulation listeners, allow aborting event handling
      if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) {
        continue;
      }

      if (event.getType() != FlightEvent.Type.ALTITUDE) {
        log.verbose("BasicEventSimulationEngine:  Handling event " + event);
      }

      if (event.getType() == FlightEvent.Type.IGNITION) {
        MotorMount mount = (MotorMount) event.getSource();
        MotorId motorId = (MotorId) event.getData();
        MotorInstance instance = status.getMotorConfiguration().getMotorInstance(motorId);
        if (!SimulationListenerHelper.fireMotorIgnition(status, motorId, mount, instance)) {
          continue;
        }
      }

      if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
        RecoveryDevice device = (RecoveryDevice) event.getSource();
        if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) {
          continue;
        }
      }

      // Check for motor ignition events, add ignition events to queue
      for (MotorId id : status.getMotorConfiguration().getMotorIDs()) {
        MotorMount mount = status.getMotorConfiguration().getMotorMount(id);
        RocketComponent component = (RocketComponent) mount;

        if (mount.getIgnitionEvent().isActivationEvent(event, component)) {
          addEvent(
              new FlightEvent(
                  FlightEvent.Type.IGNITION,
                  status.getSimulationTime() + mount.getIgnitionDelay(),
                  component,
                  id));
        }
      }

      // Check for stage separation event
      for (int stageNo : status.getConfiguration().getActiveStages()) {
        if (stageNo == 0) continue;

        Stage stage = (Stage) status.getConfiguration().getRocket().getChild(stageNo);
        if (stage.getSeparationEvent().isSeparationEvent(event, stage)) {
          addEvent(
              new FlightEvent(
                  FlightEvent.Type.STAGE_SEPARATION,
                  event.getTime() + stage.getSeparationDelay(),
                  stage));
        }
      }

      // Check for recovery device deployment, add events to queue
      Iterator<RocketComponent> rci = status.getConfiguration().iterator();
      while (rci.hasNext()) {
        RocketComponent c = rci.next();
        if (!(c instanceof RecoveryDevice)) continue;
        if (((RecoveryDevice) c).getDeployEvent().isActivationEvent(event, c)) {
          // Delay event by at least 1ms to allow stage separation to occur first
          addEvent(
              new FlightEvent(
                  FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
                  event.getTime() + Math.max(0.001, ((RecoveryDevice) c).getDeployDelay()),
                  c));
        }
      }

      // Handle event
      switch (event.getType()) {
        case LAUNCH:
          {
            status.getFlightData().addEvent(event);
            break;
          }

        case IGNITION:
          {
            // Ignite the motor
            MotorMount mount = (MotorMount) event.getSource();
            RocketComponent component = (RocketComponent) mount;
            MotorId motorId = (MotorId) event.getData();
            MotorInstanceConfiguration config = status.getMotorConfiguration();
            config.setMotorIgnitionTime(motorId, event.getTime());
            status.setMotorIgnited(true);
            status.getFlightData().addEvent(event);

            break;
          }

        case LIFTOFF:
          {
            // Mark lift-off as occurred
            status.setLiftoff(true);
            status.getFlightData().addEvent(event);
            break;
          }

        case LAUNCHROD:
          {
            // Mark launch rod as cleared
            status.setLaunchRodCleared(true);
            status.getFlightData().addEvent(event);
            break;
          }

        case BURNOUT:
          {
            // If motor burnout occurs without lift-off, abort
            if (!status.isLiftoff()) {
              throw new SimulationLaunchException("Motor burnout without liftoff.");
            }
            // Add ejection charge event
            String id = status.getConfiguration().getMotorConfigurationID();
            MotorMount mount = (MotorMount) event.getSource();
            double delay = mount.getMotorDelay(id);
            if (delay != Motor.PLUGGED) {
              addEvent(
                  new FlightEvent(
                      FlightEvent.Type.EJECTION_CHARGE,
                      status.getSimulationTime() + delay,
                      event.getSource(),
                      event.getData()));
            }
            status.getFlightData().addEvent(event);
            break;
          }

        case EJECTION_CHARGE:
          {
            status.getFlightData().addEvent(event);
            break;
          }

        case STAGE_SEPARATION:
          {
            // TODO: HIGH: Store lower stages to be simulated later
            RocketComponent stage = event.getSource();
            int n = stage.getStageNumber();
            status.getConfiguration().setToStage(n - 1);
            status.getFlightData().addEvent(event);
            break;
          }

        case APOGEE:
          // Mark apogee as reached
          status.setApogeeReached(true);
          status.getFlightData().addEvent(event);
          break;

        case RECOVERY_DEVICE_DEPLOYMENT:
          RocketComponent c = event.getSource();
          int n = c.getStageNumber();
          // Ignore event if stage not active
          if (status.getConfiguration().isStageActive(n)) {
            // TODO: HIGH: Check stage activeness for other events as well?

            // Check whether any motor in the active stages is active anymore
            for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
              int stage =
                  ((RocketComponent) status.getMotorConfiguration().getMotorMount(motorId))
                      .getStageNumber();
              if (!status.getConfiguration().isStageActive(stage)) continue;
              if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive()) continue;
              status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
            }

            // Check for launch rod
            if (!status.isLaunchRodCleared()) {
              status
                  .getWarnings()
                  .add(
                      Warning.fromString(
                          "Recovery device device deployed while on " + "the launch guide."));
            }

            // Check current velocity
            if (status.getRocketVelocity().length() > 20) {
              // TODO: LOW: Custom warning.
              status
                  .getWarnings()
                  .add(
                      Warning.fromString(
                          "Recovery device deployment at high "
                              + "speed ("
                              + UnitGroup.UNITS_VELOCITY.toStringUnit(
                                  status.getRocketVelocity().length())
                              + ")."));
            }

            status.setLiftoff(true);
            status.getDeployedRecoveryDevices().add((RecoveryDevice) c);

            this.currentStepper = this.landingStepper;
            this.status = currentStepper.initialize(status);

            status.getFlightData().addEvent(event);
          }
          break;

        case GROUND_HIT:
          status.getFlightData().addEvent(event);
          break;

        case SIMULATION_END:
          ret = false;
          status.getFlightData().addEvent(event);
          break;

        case ALTITUDE:
          break;
      }
    }

    // If no motor has ignited, abort
    if (!status.isMotorIgnited()) {
      throw new MotorIgnitionException("No motors ignited.");
    }

    return ret;
  }