/**
   * Stops an instance (prerequisite: DEPLOYED_STARTED).
   *
   * @param instance the instance
   * @param plugin the associated plug-in
   * @throws IOException if something went wrong
   */
  void stop(Instance instance, PluginInterface plugin) throws IOException {

    String instancePath = InstanceHelpers.computeInstancePath(instance);
    if (instance.getStatus() != InstanceStatus.DEPLOYED_STARTED)
      this.logger.fine(
          instancePath
              + " cannot be stopped. Prerequisite status: DEPLOYED_STARTED (but was "
              + instance.getStatus()
              + ").");
    else stopInstance(instance, plugin, false);
  }
  /**
   * Deploys an instance (prerequisite: NOT_DEPLOYED).
   *
   * @param instance the instance
   * @param plugin the associated plug-in
   * @param fileNameToFileContent a map containing resources for the plug-in
   * @throws IOException if something went wrong
   */
  void deploy(Instance instance, PluginInterface plugin, Map<String, byte[]> fileNameToFileContent)
      throws IOException {

    String instancePath = InstanceHelpers.computeInstancePath(instance);
    if (instance.getStatus() != InstanceStatus.NOT_DEPLOYED) {
      this.logger.fine(
          instancePath
              + " cannot be deployed. Prerequisite status: NOT_DEPLOYED (but was "
              + instance.getStatus()
              + ").");

    } else if (instance.getParent() != null
        && instance.getParent().getStatus() != InstanceStatus.DEPLOYED_STARTED
        && instance.getParent().getStatus() != InstanceStatus.DEPLOYED_STOPPED
        && instance.getParent().getStatus() != InstanceStatus.UNRESOLVED
        && instance.getParent().getStatus() != InstanceStatus.WAITING_FOR_ANCESTOR) {
      this.logger.fine(
          instancePath
              + " cannot be deployed because its parent is not deployed. Parent status: "
              + instance.getParent().getStatus()
              + ".");

    } else {
      this.logger.fine("Deploying instance " + instancePath);

      // User reporting => deploying...
      instance.setStatus(InstanceStatus.DEPLOYING);
      try {
        this.messagingClient.sendMessageToTheDm(
            new MsgNotifInstanceChanged(this.appName, instance));

        // Clean up the potential remains of a previous installation
        AgentUtils.deleteInstanceResources(instance);

        // Copy the resources
        AgentUtils.copyInstanceResources(instance, fileNameToFileContent);

        // Initialize the plugin
        plugin.initialize(instance);

        // Invoke the plug-in
        plugin.deploy(instance);
        instance.setStatus(InstanceStatus.DEPLOYED_STOPPED);
        this.messagingClient.sendMessageToTheDm(
            new MsgNotifInstanceChanged(this.appName, instance));

      } catch (Exception e) {
        this.logger.severe("An error occured while deploying " + instancePath);
        Utils.logException(this.logger, e);

        instance.setStatus(InstanceStatus.NOT_DEPLOYED);
        this.messagingClient.sendMessageToTheDm(
            new MsgNotifInstanceChanged(this.appName, instance));
      }
    }
  }
  /**
   * Undeploys an instance (prerequisite: DEPLOYED_STOPPED).
   *
   * @param instance the instance
   * @param plugin the associated plug-in
   * @throws IOException if something went wrong
   */
  void undeploy(Instance instance, PluginInterface plugin) throws IOException {

    // Preliminary check
    String instancePath = InstanceHelpers.computeInstancePath(instance);
    if (instance.getStatus() != InstanceStatus.DEPLOYED_STOPPED
        && instance.getStatus() != InstanceStatus.UNRESOLVED
        && instance.getStatus() != InstanceStatus.WAITING_FOR_ANCESTOR) {
      this.logger.fine(
          instancePath
              + " cannot be undeployed. Prerequisite status: DEPLOYED_STOPPED or UNRESOLVED or WAITING_FOR_ANCESTOR (but was "
              + instance.getStatus()
              + ").");
      return;
    }

    // Children may have to be marked as stopped.
    // From a plug-in point of view, we only use the one for the given instance.
    // Children are SUPPOSED to be stopped immediately.
    List<Instance> instancesToUndeploy = InstanceHelpers.buildHierarchicalList(instance);
    Collections.reverse(instancesToUndeploy);

    InstanceStatus newStatus = InstanceStatus.NOT_DEPLOYED;
    try {
      // Update the statuses if necessary
      for (Instance i : instancesToUndeploy) {
        if (i.getStatus() == InstanceStatus.NOT_DEPLOYED) continue;

        i.setStatus(InstanceStatus.UNDEPLOYING);
        this.messagingClient.sendMessageToTheDm(new MsgNotifInstanceChanged(this.appName, i));
        this.messagingClient.unpublishExports(i);
      }

      // Hypothesis: undeploying an instance undeploys its children too.
      try {
        plugin.undeploy(instance);

        // Delete files for undeployed instances
        for (Instance i : instancesToUndeploy) AgentUtils.deleteInstanceResources(i);

      } catch (PluginException e) {
        this.logger.severe(
            "An error occured while undeploying " + InstanceHelpers.computeInstancePath(instance));
        Utils.logException(this.logger, e);
        newStatus = InstanceStatus.DEPLOYED_STOPPED;
      }

    } catch (Exception e) {
      newStatus = InstanceStatus.DEPLOYED_STOPPED;
      Utils.logException(this.logger, e);

    } finally {
      // Update the status of all the instances
      for (Instance i : instancesToUndeploy) {
        i.setStatus(newStatus);
        this.messagingClient.sendMessageToTheDm(new MsgNotifInstanceChanged(this.appName, i));
      }
    }
  }
  /**
   * Builds the right handler depending on the current instance's state.
   *
   * @param instance an instance
   * @param appName the application name
   * @param messagingClient the messaging client
   * @return a non-null manager to update the instance's life cycle
   */
  public static AbstractLifeCycleManager build(
      Instance instance, String appName, IAgentClient messagingClient) {

    AbstractLifeCycleManager result;
    switch (instance.getStatus()) {
      case DEPLOYED_STARTED:
        result = new DeployedStarted(appName, messagingClient);
        break;

      case DEPLOYED_STOPPED:
        result = new DeployedStopped(appName, messagingClient);
        break;

      case NOT_DEPLOYED:
        result = new NotDeployed(appName, messagingClient);
        break;

      case UNRESOLVED:
        result = new Unresolved(appName, messagingClient);
        break;

      case WAITING_FOR_ANCESTOR:
        result = new WaitingForAncestor(appName, messagingClient);
        break;

      default:
        result = new TransitiveStates(appName, messagingClient);
        break;
    }

    return result;
  }
  /**
   * Stops an instance.
   *
   * <p>If necessary, it will stop its children.
   *
   * @param instance an instance (must be deployed and started)
   * @param plugin the plug-in to use for concrete modifications
   * @param isDueToImportsChange true if this method is called because an import changed, false it
   *     matches a manual life cycle change
   * @throws IOException if something went wrong
   */
  private void stopInstance(Instance instance, PluginInterface plugin, boolean isDueToImportsChange)
      throws IOException {

    this.logger.fine("Stopping instance " + InstanceHelpers.computeInstancePath(instance) + "...");

    // Children may have to be stopped too.
    // From a plug-in point of view, we only use the one for the given instance.
    // Children are SUPPOSED to be stopped immediately.
    List<Instance> instancesToStop = InstanceHelpers.buildHierarchicalList(instance);
    Collections.reverse(instancesToStop);

    try {
      // Update the statuses if necessary
      for (Instance i : instancesToStop) {
        if (i.getStatus() != InstanceStatus.DEPLOYED_STARTED) continue;

        i.setStatus(InstanceStatus.STOPPING);
        this.messagingClient.sendMessageToTheDm(new MsgNotifInstanceChanged(this.appName, i));
        this.messagingClient.listenToRequestsFromOtherAgents(ListenerCommand.STOP, i);
        this.messagingClient.unpublishExports(i);
      }

      // Stop the initial instance.
      // Even if the plugin invocation fails, we cannot consider these instances
      // to be reliable anymore. So, we keep on as if the operation went well.
      try {
        plugin.stop(instance);

      } catch (PluginException e) {
        this.logger.severe(
            "An error occured while stopping " + InstanceHelpers.computeInstancePath(instance));
        Utils.logException(this.logger, e);
      }

    } finally {
      List<Instance> forNotifications = new ArrayList<>();
      for (Instance i : instancesToStop) {
        if (i.getStatus() != InstanceStatus.STOPPING && i.getStatus() != InstanceStatus.UNRESOLVED)
          continue;

        // Not due to import change? => stopped.
        InstanceStatus newStatus;
        if (!isDueToImportsChange) newStatus = InstanceStatus.DEPLOYED_STOPPED;

        // If we deal with the instance whose dependencies changed => unresolved.
        else if (i == instance) newStatus = InstanceStatus.UNRESOLVED;

        // Otherwise, we deal with a child instance.
        else newStatus = InstanceStatus.WAITING_FOR_ANCESTOR;

        i.setStatus(newStatus);
        forNotifications.add(i);
      }

      // Notify at the end
      for (Instance i : forNotifications)
        this.messagingClient.sendMessageToTheDm(new MsgNotifInstanceChanged(this.appName, i));
    }
  }
  /**
   * Starts an instance (prerequisite: DEPLOYED_STOPPED).
   *
   * @param instance the instance
   * @param plugin the associated plug-in
   * @throws IOException if something went wrong
   */
  void start(Instance instance, PluginInterface plugin) throws IOException {

    String instancePath = InstanceHelpers.computeInstancePath(instance);
    if (instance.getStatus() != InstanceStatus.DEPLOYED_STOPPED
        && instance.getStatus() != InstanceStatus.WAITING_FOR_ANCESTOR) {
      this.logger.fine(
          instancePath
              + " cannot be started. Prerequisite status: DEPLOYED_STOPPED or WAITING_FOR_ANCESTOR (but was "
              + instance.getStatus()
              + ").");

    } else if (instance.getParent() != null
        && instance.getParent().getStatus() != InstanceStatus.DEPLOYED_STARTED) {

      // An element cannot start if its parent is not started.
      // However, if the parent is unresolved (or waiting for its parent to start),
      // then we can mark this instance as ready to start with its parent.
      this.logger.fine(
          instancePath
              + " cannot be started because its parent is not started. Parent status: "
              + instance.getParent().getStatus()
              + ".");
      if (instance.getParent().getStatus() == InstanceStatus.UNRESOLVED
          || instance.getParent().getStatus() == InstanceStatus.WAITING_FOR_ANCESTOR) {

        instance.setStatus(InstanceStatus.WAITING_FOR_ANCESTOR);
        this.logger.fine(instancePath + " will start as soon as its parent starts.");
      }

    } else {
      // Otherwise, start it
      try {
        if (ImportHelpers.hasAllRequiredImports(instance, this.logger)) {
          instance.data.put(FORCE, "whatever");
          updateStateFromImports(instance, plugin, null, null);

        } else {
          this.logger.fine(
              "Instance "
                  + InstanceHelpers.computeInstancePath(instance)
                  + " cannot be started, dependencies are missing. Requesting exports from other agents.");

          instance.setStatus(InstanceStatus.UNRESOLVED);
          this.messagingClient.sendMessageToTheDm(
              new MsgNotifInstanceChanged(this.appName, instance));
          this.messagingClient.requestExportsFromOtherAgents(instance);
        }

      } catch (PluginException e) {
        this.logger.severe(
            "An error occured while starting " + InstanceHelpers.computeInstancePath(instance));
        Utils.logException(this.logger, e);

        instance.setStatus(InstanceStatus.DEPLOYED_STOPPED);
        this.messagingClient.sendMessageToTheDm(
            new MsgNotifInstanceChanged(this.appName, instance));
      }
    }
  }
  /**
   * Updates the status of an instance based on the imports.
   *
   * @param impactedInstance the instance whose imports may have changed
   * @param plugin the plug-in to use to apply a concrete modification
   * @param statusChanged The changed status of the instance that changed (e.g. that provided new
   *     imports)
   * @param importChanged The individual imports that changed
   */
  public void updateStateFromImports(
      Instance impactedInstance,
      PluginInterface plugin,
      Import importChanged,
      InstanceStatus statusChanged)
      throws IOException, PluginException {

    // Do we have all the imports we need?
    boolean haveAllImports = ImportHelpers.hasAllRequiredImports(impactedInstance, this.logger);

    // Update the life cycle of this instance if necessary
    // Maybe we have something to start
    if (haveAllImports) {
      if (impactedInstance.getStatus() == InstanceStatus.UNRESOLVED
          || impactedInstance.data.remove(FORCE) != null) {

        InstanceStatus oldState = impactedInstance.getStatus();
        impactedInstance.setStatus(InstanceStatus.STARTING);
        try {
          this.messagingClient.sendMessageToTheDm(
              new MsgNotifInstanceChanged(this.appName, impactedInstance));
          plugin.start(impactedInstance);
          impactedInstance.setStatus(InstanceStatus.DEPLOYED_STARTED);

          this.messagingClient.sendMessageToTheDm(
              new MsgNotifInstanceChanged(this.appName, impactedInstance));
          this.messagingClient.publishExports(impactedInstance);
          this.messagingClient.listenToRequestsFromOtherAgents(
              ListenerCommand.START, impactedInstance);

        } catch (Exception e) {
          this.logger.severe(
              "An error occured while starting "
                  + InstanceHelpers.computeInstancePath(impactedInstance));
          Utils.logException(this.logger, e);
          impactedInstance.setStatus(oldState);
          this.messagingClient.sendMessageToTheDm(
              new MsgNotifInstanceChanged(this.appName, impactedInstance));
        }

      } else if (impactedInstance.getStatus() == InstanceStatus.DEPLOYED_STARTED) {
        plugin.update(impactedInstance, importChanged, statusChanged);

      } else {
        this.logger.fine(
            InstanceHelpers.computeInstancePath(impactedInstance)
                + " checked import changes but has nothing to update (1).");
      }
    }

    // Or maybe we have something to stop
    else {
      if (impactedInstance.getStatus() == InstanceStatus.DEPLOYED_STARTED) {
        stopInstance(impactedInstance, plugin, true);

      } else {
        this.logger.fine(
            InstanceHelpers.computeInstancePath(impactedInstance)
                + " checked import changes but has nothing to update (2).");
      }
    }
  }