/******************************************************************************
 * Copyright (C) 2010-2016 CERN. All rights not expressly granted are reserved.
 *
 * This file is part of the CERN Control and Monitoring Platform 'C2MON'.
 * C2MON is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the license.
 *
 * C2MON is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with C2MON. If not, see <http://www.gnu.org/licenses/>.
 *****************************************************************************/
package cern.c2mon.daq.common.conf.core;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;

import cern.c2mon.daq.common.conf.equipment.*;
import cern.c2mon.daq.common.messaging.ProcessRequestSender;
import cern.c2mon.daq.common.timer.FreshnessMonitor;
import cern.c2mon.daq.config.DaqProperties;
import cern.c2mon.daq.tools.StackTraceHelper;
import cern.c2mon.daq.tools.processexceptions.ConfUnknownTypeException;
import cern.c2mon.shared.common.ConfigurationException;
import cern.c2mon.shared.common.command.ISourceCommandTag;
import cern.c2mon.shared.common.command.SourceCommandTag;
import cern.c2mon.shared.common.datatag.ISourceDataTag;
import cern.c2mon.shared.common.datatag.SourceDataTag;
import cern.c2mon.shared.common.process.EquipmentConfiguration;
import cern.c2mon.shared.common.process.ProcessConfiguration;
import cern.c2mon.shared.common.process.SubEquipmentConfiguration;
import cern.c2mon.shared.daq.config.*;
import cern.c2mon.shared.daq.config.ChangeReport.CHANGE_STATE;
import cern.c2mon.shared.daq.process.ProcessConfigurationResponse;
import cern.c2mon.shared.daq.process.ProcessConnectionResponse;

/**
 * The ConfigurationController managing the configuration life cycle and allows
 * to access and change the current configuration.
 *
 * @author Andreas Lang
 * @author vilches (refactoring updates)
 */
@Slf4j
@Component("configurationController")
public class ConfigurationController {

  @Autowired
  private DaqProperties properties;

  @Setter
  @Autowired
  FreshnessMonitor freshnessMonitor;

  private long startUp;

  /**
   * The process configuration loader used at startup.
   */
  @Autowired
  private ProcessConfigurationLoader processConfigurationLoader;

  /**
   * The updater which applies changes to source data tags.
   */
  private final ConfigurationUpdater configurationUpdater = new ConfigurationUpdater();

  /**
   * Reference to the ProcessRequestSender (for requesting the XML config
   * document). This reference is injected in the Spring xml file, for ease of
   * configuration.
   */
  @Autowired
  @Qualifier("primaryRequestSender")
  private ProcessRequestSender primaryRequestSender;

  /**
   * Request sender for disconnection notifications only. Can be null!
   */
  @Autowired(required = false)
  @Qualifier("secondaryRequestSender")
  private ProcessRequestSender secondaryRequestSender;

  @Autowired
  private EquipmentConfigurationFactory equipmentConfigurationFactory;

  /**
   * Map of data tag changers. It maps equipment id - > changer.
   */
  private final Map<Long, IDataTagChanger> dataTagChangers = new ConcurrentHashMap<>();
  /**
   * Map of command tag changers. It maps equipment id - > changer.
   */
  private final  Map<Long, ICommandTagChanger> commandTagChangers = new ConcurrentHashMap<>();
  /**
   * Map of equipment changers. It maps equipment id - > changer.
   */
  private final Map<Long, IEquipmentConfigurationChanger> equipmentChangers = new ConcurrentHashMap<>();
  /**
   * These are additional changers of the core. Which can be used to inform
   * other parts of the core about changes.
   */
  private final Map<Long, List<ICoreDataTagChanger>> coreDataTagChangers = new ConcurrentHashMap<>();
  /**
   * These are additional changers of the core. Which can be used to inform
   * other parts of the core about changes.
   */
  private final Map<Long, List<ICoreCommandTagChanger>> coreCommandTagChangers = new ConcurrentHashMap<>();
  /**
   * The core equipment configuration changers.
   */
  private final Map<Long, List<ICoreEquipmentConfigurationChanger>> coreEquipmentConfigurationChangers = new HashMap<>();

  /**
   * Loads configurations: Process PIK and Process Configuration. Catches
   * RuntimeException coming from errors while parsing PIK or Configurations XML
   * files.
   */
  public void initProcess() {
    this.startUp = System.currentTimeMillis();

    try {
      // Get the PIK from the server
      log.trace("initProcess - Process Connection called.");
      this.loadProcessConnection();

      // Configuration

      log.trace("initProcess - Process Configuration called.");
      this.loadProcessConfiguration();

    } catch (Exception ex) {
      throw new RuntimeException("Exception caught during DAQ startup", ex);
    }
  }

  /**
   * Loads the Process Connection with the identification key (PIK) from the
   * server. If the PIK request is rejected from the server the start up process
   * stops.
   */
  public void loadProcessConnection() {
    // Get the PIK from the server
    ProcessConnectionResponse processConnectionResponse = processConfigurationLoader.getProcessConnection();

    // If Process PIK is REJECTED we exit
    if (processConnectionResponse.getProcessPIK() == null || processConnectionResponse.getProcessPIK() <= ProcessConnectionResponse.PIK_REJECTED) {
      System.err.println("\nConnection rejected for process " + processConnectionResponse.getProcessName() +
              ": Either the process is already running or it didn't shut down cleanly. " +
              "Please stop the existing process or wait until its heartbeat expires.\n");
      System.exit(1);
    }

    // Set process PIK for future communications with the server (if it exists)
    // in a provisional ProcessConfiguration
    ProcessConfiguration processConfiguration = new ProcessConfiguration();
    processConfiguration.setProcessName(processConnectionResponse.getProcessName());
    processConfiguration.setprocessPIK(processConnectionResponse.getProcessPIK());

    ProcessConfigurationHolder.setInstance(processConfiguration);
  }

  /**
   * Loads the process configuration.
   */
  public void loadProcessConfiguration() {
    Document xmlConfiguration;
    ProcessConfigurationResponse processConfigurationResponse = null;
    log.trace("Configuration process started");

    String localConfigFile = properties.getLocalConfigFile();
    // e.g. $DAQ_HOME/conf/local/P_TEST.xml
    File defaultLocalConfigFile = new File(System.getProperty("user.dir") + "/conf/local/" + properties.getName().toUpperCase() + ".xml");

    boolean localConfiguration = true;

    if (localConfigFile != null) {
      xmlConfiguration = loadFromLocalConfigFile(localConfigFile);
    }
    else if (defaultLocalConfigFile.exists()) {
      xmlConfiguration = loadFromLocalConfigFile(defaultLocalConfigFile.toString());
    }
    else {
      localConfiguration = false;
      log.info("Loading configuration from server");
      processConfigurationResponse = this.processConfigurationLoader.getProcessConfiguration();

      // If Process Configuration is REJECTED we exit
      if (processConfigurationResponse.getConfigurationXML().equals(ProcessConfigurationResponse.CONF_REJECTED)) {
        sendDisconnectionNotification();
        throw new RuntimeException("CONF_REJECTED received");
      }

      // processConfigurationResponse will never be null at this point
      xmlConfiguration = this.processConfigurationLoader.fromXMLtoDOC(processConfigurationResponse.getConfigurationXML());
    }

    // If XML Configuration is wrong and cannot be parsed we exit
    if (xmlConfiguration == null) {
      sendDisconnectionNotification();
      throw new RuntimeException("Could not parse XML configuration");
    }

    // Save config if it was the option and it is not local config (pointless)
    if (properties.getSaveRemoteConfig() != null) {
      if (!localConfiguration) {
        saveConfiguration(xmlConfiguration);
      } else {
        log.info("Local configuration will not be saved. It is already in local disk");
      }
    }

    log.debug("Loading DAQ configuration properties from XML document...");

    // try to create process configuration object (with the PIK saved in the
    // provisional ProcessConfiguration)
    try {
      ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();
      configuration = this.processConfigurationLoader.createProcessConfiguration(configuration.getProcessName(),
          configuration.getprocessPIK(), xmlConfiguration);
      ProcessConfigurationHolder.setInstance(configuration);

      log.debug("Process configuration successfully loaded");
    } catch (ConfUnknownTypeException ex) {
      sendDisconnectionNotification();
      throw new RuntimeException("UNKNOWN configuration received");

    } catch (Exception ex) {
      sendDisconnectionNotification();
      throw new RuntimeException("Exception caught while configuring the DAQ. Check the configuration XML", ex);
    }
  }

  /**
   * Helper method to load configuration from local file
   */
  private Document loadFromLocalConfigFile(String localConfigFile) {
    log.info("Loading configuration from file: {}", localConfigFile);
    return this.processConfigurationLoader.fromFiletoDOC(localConfigFile);
  }

  /**
   * Saves the process configuration.
   */
  private void saveConfiguration(Document docXMLConfig) {
    String fileToSaveConf = properties.getSaveRemoteConfig();
    if (fileToSaveConf.length() > 0 && docXMLConfig != null) {
      log.info("saveConfiguration - saving the process configuration XML in a file " + fileToSaveConf + " due to user request");

      File file = new File(fileToSaveConf);
      if (file.isDirectory() || !fileToSaveConf.endsWith(".xml")) {
        throw new RuntimeException("Path to which to save remote config must end with '.xml'");
      }

      try {
        DOMImplementationLS domImplementation = (DOMImplementationLS) docXMLConfig.getImplementation();
        LSSerializer lsSerializer = domImplementation.createLSSerializer();
        lsSerializer.writeToURI(docXMLConfig, file.toURI().toURL().toString());
      } catch (java.io.IOException ex) {
        log.error("saveConfiguration - Could not save the configuration to the file " + fileToSaveConf, ex);
      }
    }
  }

  /**
   * Sends disconnection notifications to all request senders.
   */
  private void sendDisconnectionNotification() {
    log.trace("sendDisconnectionNotification - Primary Request Sender disconnection");
    primaryRequestSender.sendProcessDisconnectionRequest(ProcessConfigurationHolder.getInstance(), startUp);

    // send in separate thread as may block if broker problem
    if (secondaryRequestSender != null) {
      log.trace("sendDisconnectionNotification - Secondary Request Sender disconnection (new thread)");
      Thread disconnectSend = new Thread(new Runnable() {
        @Override
        public void run() {
          secondaryRequestSender.sendProcessDisconnectionRequest(ProcessConfigurationHolder.getInstance(), startUp);
        }
      });
      disconnectSend.setDaemon(true);
      disconnectSend.start();
    }
  }

  /**
   * This is called if a data tag should be added to the configuration. It
   * applies the changes to the core and calls then the lower layer to also
   * perform the changes.
   *
   * @param dataTagAddChange The data tag add change.
   *
   * @return A report with information if the change was successful.
   */
  public synchronized ChangeReport onDataTagAdd(final DataTagAdd dataTagAddChange) {
    log.debug("onDataTagAdd - entering onDataTagAdd()");
    log.debug("changeId: " + dataTagAddChange.getChangeId());

    ChangeReport changeReport = new ChangeReport(dataTagAddChange);
    Long equipmentId = dataTagAddChange.getEquipmentId();

    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    // Check if the equipment id is a SubEquipment id.
    if (!configuration.getEquipmentConfigurations().containsKey(equipmentId)) {
      for (EquipmentConfiguration equipmentConfiguration : configuration.getEquipmentConfigurations().values()) {
        if (equipmentConfiguration.getSubEquipmentConfigurations().containsKey(equipmentId)) {
          equipmentId = equipmentConfiguration.getId();
        }
      }
    }

    SourceDataTag sourceDataTag = dataTagAddChange.getSourceDataTag();
    Long dataTagId = sourceDataTag.getId();
    Map<Long, SourceDataTag> sourceDataTags = getSourceDataTags(equipmentId);
    if (sourceDataTags == null) {
      log.warn("cannot add data tag - equipment id: " + dataTagAddChange.getEquipmentId() + " is unknown");
      changeReport.appendError("Equipment does not exist: " + equipmentId);
      return changeReport;
    }
    try {
      sourceDataTag.validate();
    } catch (ConfigurationException e) {
      changeReport.appendError("Error validating data tag");
      changeReport.appendError(StackTraceHelper.getStackTrace(e));
      return changeReport;
    }

    if (sourceDataTags.containsKey(dataTagId)) {

      log.warn("onDataTagAdd - cannot add data tag id: " + dataTagId + " to equipment id: " + dataTagAddChange.getEquipmentId() + " This equipment already" +
          " has tag with that id");

      changeReport.appendError("DataTag " + dataTagId + " is already in equipment " + equipmentId);
    } else {
      sourceDataTags.put(dataTagId, sourceDataTag);
      changeReport.appendInfo("Core added data tag with id " + sourceDataTag.getId() + " successfully to equipment " + equipmentId);
      List<ICoreDataTagChanger> coreChangers = coreDataTagChangers.get(equipmentId);
      if (coreChangers != null) {
        for (ICoreDataTagChanger dataTagChanger : coreChangers) {
          dataTagChanger.onAddDataTag(sourceDataTag, changeReport);
        }
      }
      IDataTagChanger dataTagChanger = dataTagChangers.get(equipmentId);
      if (dataTagChanger != null) {
        dataTagChanger.onAddDataTag(sourceDataTag, changeReport);
        // changeReport.setState(CHANGE_STATE.SUCCESS);
      } else {
        changeReport.appendError("It was not possible to apply the changes" + "to the implementation part. No data tag changer was found.");
        changeReport.setState(CHANGE_STATE.REBOOT);
      }
    }
    log.debug("onDataTagAdd - exiting onDataTagAdd()");
    return changeReport;
  }

  /**
   * This is called if a command tag should be added to the configuration. It
   * applies the changes to the core and calls then the lower layer to also
   * perform the changes.
   *
   * @param commandTagAddChange The command tag add change.
   *
   * @return A report with information if the change was successful.
   */
  public synchronized ChangeReport onCommandTagAdd(final CommandTagAdd commandTagAddChange) {
    log.debug("entering onCommandTagAdd()");
    log.debug("changeId: " + commandTagAddChange.getChangeId());

    ChangeReport changeReport = new ChangeReport(commandTagAddChange);
    Long equipmentId = commandTagAddChange.getEquipmentId();

    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    // Check if the equipment id is a SubEquipment id.
    if (!configuration.getEquipmentConfigurations().containsKey(equipmentId)) {
      for (EquipmentConfiguration equipmentConfiguration : configuration.getEquipmentConfigurations().values()) {
        if (equipmentConfiguration.getSubEquipmentConfigurations().containsKey(equipmentId)) {
          equipmentId = equipmentConfiguration.getId();
        }
      }
    }

    Map<Long, SourceCommandTag> sourceCommandTags = getSourceCommandTags(equipmentId);
    if (sourceCommandTags == null) {
      log.warn("cannot add command tag - equipment id: " + commandTagAddChange.getEquipmentId() + " is unknown");
      changeReport.appendError("Equipment does not exist: " + equipmentId);
      return changeReport;
    }
    SourceCommandTag sourceCommandTag = commandTagAddChange.getSourceCommandTag();
    try {
      sourceCommandTag.validate();
    } catch (ConfigurationException e) {
      changeReport.appendError("Error validating command tag");
      changeReport.appendError(StackTraceHelper.getStackTrace(e));
      return changeReport;
    }
    Long commandTagId = sourceCommandTag.getId();
    if (sourceCommandTags.containsKey(commandTagId)) {

      log.warn("cannot add command tag id: " + commandTagId + " to equipment id: " + commandTagAddChange.getEquipmentId() + " This equipment already has " +
          "tag with that id");

      changeReport.appendError("CommandTag " + commandTagId + " is already in equipment " + equipmentId);
    } else {
      sourceCommandTags.put(sourceCommandTag.getId(), sourceCommandTag);
      changeReport.appendInfo("Core added command tag with id " + sourceCommandTag.getId() + " successfully to equipment " + equipmentId);
      List<ICoreCommandTagChanger> coreChangers = coreCommandTagChangers.get(equipmentId);
      if (coreChangers != null) {
        for (ICoreCommandTagChanger commandTagChanger : coreChangers) {
          commandTagChanger.onAddCommandTag(sourceCommandTag, changeReport);
        }
      }
      ICommandTagChanger commandTagChanger = commandTagChangers.get(equipmentId);
      if (commandTagChanger != null) {
        commandTagChanger.onAddCommandTag(sourceCommandTag, changeReport);
        // changeReport.setState(CHANGE_STATE.SUCCESS);
      } else {
        changeReport.appendError("It was not possible to apply the changes" + "to the implementation part. No command tag changer was found.");
        changeReport.setState(CHANGE_STATE.REBOOT);
      }
    }
    return changeReport;
  }

  /**
   * Removes a data tag from an equipment.
   *
   * @param dataTagRemoveChange The change with all the data to remove the tag.
   *
   * @return A change report with success information.
   */
  public synchronized ChangeReport onDataTagRemove(final DataTagRemove dataTagRemoveChange) {
    log.debug("Entering onDataTagRemove: ");

    ChangeReport changeReport = new ChangeReport(dataTagRemoveChange);
    Long equipmentId = dataTagRemoveChange.getEquipmentId();
    Map<Long, SourceDataTag> sourceDataTags = getSourceDataTags(equipmentId);
    if (sourceDataTags == null) {
      changeReport.appendError("Equipment does not exist: " + equipmentId);
      return changeReport;
    }

    log.debug("onDataTagRemove - removing " + dataTagRemoveChange.getDataTagId());
    SourceDataTag sourceDataTag = sourceDataTags.get(dataTagRemoveChange.getDataTagId());

    if (sourceDataTag != null) {
      log.debug("onDataTagRemove - Core removed data tag with id " + dataTagRemoveChange.getDataTagId() + " successfully from equipment " + equipmentId);
      changeReport.appendInfo("Core removed data tag with id " + dataTagRemoveChange.getDataTagId() + " successfully from equipment " + equipmentId);
      List<ICoreDataTagChanger> coreChangers = coreDataTagChangers.get(equipmentId);

      if (coreChangers != null) {
        for (ICoreDataTagChanger dataTagChanger : coreChangers) {
          dataTagChanger.onRemoveDataTag(sourceDataTag, changeReport);
        }

      }
      IDataTagChanger dataTagChanger = dataTagChangers.get(equipmentId);
      if (dataTagChanger != null) {
        dataTagChanger.onRemoveDataTag(sourceDataTag, changeReport);
        // changeReport.setState(CHANGE_STATE.SUCCESS);
      } else {
        changeReport.appendError("It was not possible to apply the changes" + "to the implementation part. No data tag changer was found.");
        changeReport.setState(CHANGE_STATE.REBOOT);
      }

      freshnessMonitor.removeDataTag(sourceDataTag);

      // remove the tag from the core's map
      sourceDataTags.remove(dataTagRemoveChange.getDataTagId());

    } else {
      log.debug("onDataTagRemove - The data tag with id " + dataTagRemoveChange.getDataTagId() + " to remove was not found" + " in equipment with id " +
          equipmentId);
      // The data tag which should be removed was not found which means the same
      // result as foudn and removed.
      changeReport.appendWarn("The data tag with id " + dataTagRemoveChange.getDataTagId() + " to remove was not found" + " in equipment with id " +
          equipmentId);
      changeReport.setState(CHANGE_STATE.SUCCESS);
    }

    log.debug("Exiting onDataTagRemove: ");

    return changeReport;
  }

  /**
   * Updates a data tag.
   *
   * @param dataTagUpdateChange The object with all necessary to update the tag.
   *
   * @return A change report containing information about the success of the
   * update.
   */
  public synchronized ChangeReport onDataTagUpdate(final DataTagUpdate dataTagUpdateChange) {
    ChangeReport changeReport = new ChangeReport(dataTagUpdateChange);
    long equipmentId = dataTagUpdateChange.getEquipmentId();

    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    // Check if the equipment id is a SubEquipment id.
    if (!configuration.getEquipmentConfigurations().containsKey(equipmentId)) {
      for (EquipmentConfiguration equipmentConfiguration : configuration.getEquipmentConfigurations().values()) {
        if (equipmentConfiguration.getSubEquipmentConfigurations().containsKey(equipmentId)) {
          equipmentId = equipmentConfiguration.getId();
        }
      }
    }

    long dataTagId = dataTagUpdateChange.getDataTagId();
    Map<Long, SourceDataTag> sourceDataTags = getSourceDataTags(equipmentId);
    if (sourceDataTags == null) {
      changeReport.appendError("Equipment does not exists: " + equipmentId);
      return changeReport;
    }
    if (sourceDataTags.containsKey(dataTagId)) {
      try {
        SourceDataTag sourceDataTag = sourceDataTags.get(dataTagId);
        SourceDataTag oldSourceDataTag = sourceDataTag.clone();
        synchronized (sourceDataTag) {
          configurationUpdater.updateDataTag(dataTagUpdateChange, sourceDataTag);
        }
        try {
          sourceDataTag.validate();
        } catch (ConfigurationException e) {
          sourceDataTags.put(dataTagId, oldSourceDataTag);
          changeReport.appendError("Error validating data tag");
          changeReport.appendError(StackTraceHelper.getStackTrace(e));
          return changeReport;
        }
        changeReport.appendInfo("Core Data Tag update successfully applied.");
        IDataTagChanger dataTagChanger = dataTagChangers.get(equipmentId);
        dataTagChanger.onUpdateDataTag(sourceDataTag, oldSourceDataTag, changeReport);
        if (changeReport.getState().equals(CHANGE_STATE.SUCCESS)) {
          List<ICoreDataTagChanger> coreChangers = coreDataTagChangers.get(equipmentId);
          if (coreChangers != null) {
            // I do it here to avoid putting them back in the old state after an
            // error
            for (ICoreDataTagChanger coreDataTagChanger : coreChangers) {
              coreDataTagChanger.onUpdateDataTag(sourceDataTag, oldSourceDataTag, changeReport);
            }
          }
          changeReport.appendInfo("Change fully applied.");
        } else {
          sourceDataTags.put(dataTagId, oldSourceDataTag);
        }
      } catch (Exception e) {
        changeReport.appendError("Error while applying data tag changes\n" + StackTraceHelper.getStackTrace(e));
      }
    } else {
      changeReport.appendError("Data Tag " + dataTagId + " to update was not found.");
    }
    return changeReport;
  }

  /**
   * Removes a command tag from an equipment.
   *
   * @param commandTagRemoveChange The change object with all the information to
   *                               remove the command tag.
   *
   * @return A report with information about success of the change.
   */
  public synchronized ChangeReport onCommandTagRemove(final CommandTagRemove commandTagRemoveChange) {
    ChangeReport changeReport = new ChangeReport(commandTagRemoveChange);
    Long equipmentId = commandTagRemoveChange.getEquipmentId();
    Map<Long, SourceCommandTag> sourceCommandTags = getSourceCommandTags(equipmentId);
    if (sourceCommandTags == null) {
      changeReport.appendError("Equipment does not exists: " + equipmentId);
      return changeReport;
    }
    SourceCommandTag sourceCommandTag = sourceCommandTags.remove(commandTagRemoveChange.getCommandTagId());
    if (sourceCommandTag != null) {
      changeReport.appendInfo("Core removed command tag with id " + commandTagRemoveChange.getCommandTagId() + " successfully from equipment " + equipmentId);
      List<ICoreCommandTagChanger> coreChangers = coreCommandTagChangers.get(equipmentId);
      if (coreChangers != null) {
        for (ICoreCommandTagChanger commandTagChanger : coreChangers) {
          commandTagChanger.onRemoveCommandTag(sourceCommandTag, changeReport);
        }
      }
      ICommandTagChanger commandTagChanger = commandTagChangers.get(equipmentId);
      if (commandTagChanger != null) {
        commandTagChanger.onRemoveCommandTag(sourceCommandTag, changeReport);
        // changeReport.setState(CHANGE_STATE.SUCCESS);
      } else {
        changeReport.appendError("It was not possible to apply the changes" + " to the implementation part. No command tag changer was found.");
        changeReport.setState(CHANGE_STATE.REBOOT);
      }
    } else {
      // The command tag which should be removed was not found which means the
      // same result as foudn and removed.
      changeReport.appendWarn("The command tag with id " + commandTagRemoveChange.getCommandTagId() + " to remove was not found" + " in equipment with id " +
          equipmentId);
      changeReport.setState(CHANGE_STATE.SUCCESS);
    }
    return changeReport;
  }

  /**
   * Updates a command tag.
   *
   * @param commandTagUpdateChange The object with all the information to update
   *                               the tag.
   *
   * @return A change report with information about the success of the update.
   */
  public synchronized ChangeReport onCommandTagUpdate(final CommandTagUpdate commandTagUpdateChange) {
    ChangeReport changeReport = new ChangeReport(commandTagUpdateChange);
    long equipmentId = commandTagUpdateChange.getEquipmentId();
    long commandTagId = commandTagUpdateChange.getCommandTagId();

    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    // Check if the equipment id is a SubEquipment id.
    if (!configuration.getEquipmentConfigurations().containsKey(equipmentId)) {
      for (EquipmentConfiguration equipmentConfiguration : configuration.getEquipmentConfigurations().values()) {
        if (equipmentConfiguration.getSubEquipmentConfigurations().containsKey(equipmentId)) {
          equipmentId = equipmentConfiguration.getId();
        }
      }
    }

    Map<Long, SourceCommandTag> sourceCommandTags = getSourceCommandTags(equipmentId);
    if (sourceCommandTags == null) {
      changeReport.appendError("Equipment does not exists: " + equipmentId);
      return changeReport;
    }
    if (sourceCommandTags.containsKey(commandTagId)) {
      try {
        SourceCommandTag sourceCommandTag = sourceCommandTags.get(commandTagId);
        SourceCommandTag oldSourceCommandTag = sourceCommandTag.clone();
        synchronized (sourceCommandTag) {
          configurationUpdater.updateCommandTag(commandTagUpdateChange, sourceCommandTag);
          try {
            sourceCommandTag.validate();
          } catch (ConfigurationException e) {
            sourceCommandTags.put(commandTagId, oldSourceCommandTag);
            changeReport.appendError("Error validating command tag");
            changeReport.appendError(StackTraceHelper.getStackTrace(e));
            return changeReport;
          }
        }
        changeReport.appendInfo("Core Command Tag update successfully applied.");
        ICommandTagChanger commandTagChanger = commandTagChangers.get(equipmentId);
        commandTagChanger.onUpdateCommandTag(sourceCommandTag, oldSourceCommandTag, changeReport);
        if (changeReport.getState().equals(CHANGE_STATE.SUCCESS)) {
          List<ICoreCommandTagChanger> coreChangers = coreCommandTagChangers.get(equipmentId);
          if (coreChangers != null) {
            for (ICoreCommandTagChanger coreCommandTagChanger : coreChangers) {
              coreCommandTagChanger.onUpdateCommandTag(sourceCommandTag, oldSourceCommandTag, changeReport);
            }
          }
          changeReport.appendInfo("Change fully applied.");
        } else {
          sourceCommandTags.put(commandTagId, oldSourceCommandTag);
        }
      } catch (Exception e) {
        changeReport.appendError("Error while applying command tag changes: " + e.getMessage());
      }
    } else {
      changeReport.appendError("Command Tag " + commandTagId + " to update was not found.");
    }
    return changeReport;
  }

  /**
   * Updates the equipment configuration with the new values in the provided
   * EquipmentConfigurationUpdate.
   *
   * @param equipmentConfigurationUpdate The update with the changed values.
   *
   * @return A change report with information about the success of the update.
   */
  public synchronized ChangeReport onEquipmentConfigurationUpdate(final EquipmentConfigurationUpdate equipmentConfigurationUpdate) {
    long equipmentId = equipmentConfigurationUpdate.getEquipmentId();
    ChangeReport changeReport = new ChangeReport(equipmentConfigurationUpdate);
    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    try {
      EquipmentConfiguration equipmentConfiguration = configuration.getEquipmentConfiguration(equipmentId);
      if (equipmentConfiguration != null) {
        EquipmentConfiguration clonedEquipmentConfiguration = equipmentConfiguration.clone();
        synchronized (equipmentConfiguration) {
          configurationUpdater.updateEquipmentConfiguration(equipmentConfigurationUpdate, equipmentConfiguration);
        }
        IEquipmentConfigurationChanger equipmentConfigurationChanger = equipmentChangers.get(equipmentId);
        equipmentConfigurationChanger.onUpdateEquipmentConfiguration(equipmentConfiguration, clonedEquipmentConfiguration, changeReport);
        if (changeReport.getState().equals(CHANGE_STATE.SUCCESS)) {
          List<ICoreEquipmentConfigurationChanger> coreChangers = coreEquipmentConfigurationChangers.get(equipmentId);
          if (coreChangers != null) {
            for (ICoreEquipmentConfigurationChanger equipmentChanger : coreChangers) {
              equipmentChanger.onUpdateEquipmentConfiguration(equipmentConfiguration, clonedEquipmentConfiguration, changeReport);
            }
          }
          // I do it here to avoid putting them back in the old state after an
          // error
          changeReport.appendInfo("Change fully applied.");
        } else {
          configuration.getEquipmentConfigurations().put(equipmentId, clonedEquipmentConfiguration);
        }
      } else {
        changeReport.appendError("Equipment configuration with id: " + equipmentId + " not found.");
      }
    } catch (Exception e) {
      changeReport.appendError("Error while applying equipment changes: " + e.getMessage());
    }
    return changeReport;
  }

  //
  // public EquipmentConfiguration createEquipmentConfiguration(EquipmentUnitAdd
  // equipmentUnitAdd) {
  // return
  // processConfigurationLoader.createEquipmentConfiguration(equipmentUnitAdd);
  // }

  /**
   * Updates the process configuration with the new values provided in the
   * ProcessConfigurationUpdate.
   *
   * @param processConfigurationUpdate The update with the changed values.
   *
   * @return A change report with information about the success of the update.
   */
  public synchronized ChangeReport onProcessConfigurationUpdate(final ProcessConfigurationUpdate processConfigurationUpdate) {
    ChangeReport changeReport = new ChangeReport(processConfigurationUpdate);
    long processId = processConfigurationUpdate.getProcessId();
    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    try {
      if (processId == configuration.getProcessID()) {
        synchronized (configuration) {
          configurationUpdater.updateProcessConfiguration(processConfigurationUpdate, configuration);
        }
        changeReport.appendInfo("Process with id " + processId + " successfully updated.");
        changeReport.setState(CHANGE_STATE.SUCCESS);
      } else {
        changeReport.appendError("The process id of this DAQ is " + configuration.getProcessID() + " not " + processId + ".");
      }
    } catch (Exception e) {
      changeReport.appendError("Error while applying process changes: " + e.getMessage());
    }
    return changeReport;
  }

  /**
   * Updates the DAQ by removing a whole SubEquipment.
   *
   * @param subEquipmentUnitRemove the subequipment unit to be removed
   *
   * @return a change report with information about the success of the update.
   */
  public ChangeReport onSubEquipmentUnitRemove(SubEquipmentUnitRemove subEquipmentUnitRemove) {
    log.debug("onSubEquipmentUnitRemove - entering onSubEquipmentUnitRemove()..");
    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    ChangeReport changeReport = new ChangeReport(subEquipmentUnitRemove);
    changeReport.setState(CHANGE_STATE.SUCCESS);

    // Check if the parent equipment exists
    EquipmentConfiguration parentEquipmentConfiguration = configuration.getEquipmentConfiguration(subEquipmentUnitRemove.getParentEquipmentId());
    if (parentEquipmentConfiguration == null) {
      changeReport.appendError("Parent Equipment unit id: " + subEquipmentUnitRemove.getParentEquipmentId() + " for SubEquipment unit " +
          subEquipmentUnitRemove.getSubEquipmentId() + " is unknown");
      changeReport.setState(CHANGE_STATE.FAIL);
      return changeReport;
    }

    // Find the SubEquipment configuration
    SubEquipmentConfiguration subEquipmentConfiguration = parentEquipmentConfiguration.getSubEquipmentConfiguration(subEquipmentUnitRemove.getSubEquipmentId());
    if (subEquipmentConfiguration == null) {
      changeReport.appendWarn("SubEquipment unit id: " + subEquipmentUnitRemove.getSubEquipmentId() + " is unknown");
    } else {
      parentEquipmentConfiguration.getSubEquipmentConfigurations().remove(subEquipmentConfiguration.getId());
    }

    return changeReport;
  }

  /**
   * Updates the DAQ by injecting a new SubEquipment Unit.
   *
   * @param subEquipmentUnitAdd the newly injected sub equipment unit
   *
   * @return a change report with information about the success of the update.
   */

  public ChangeReport onSubEquipmentUnitAdd(final SubEquipmentUnitAdd subEquipmentUnitAdd) {
    log.debug("onSubEquipmentUnitAdd - entering onSubEquipmentUnitAdd()..");
    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();

    ChangeReport changeReport = new ChangeReport(subEquipmentUnitAdd);
    changeReport.setState(CHANGE_STATE.SUCCESS);

    // Check if the parent equipment exists
    EquipmentConfiguration parentEquipmentConfiguration = configuration.getEquipmentConfiguration(subEquipmentUnitAdd.getParentEquipmentId());
    if (parentEquipmentConfiguration == null) {
      changeReport.appendError("Parent Equipment unit id: " + subEquipmentUnitAdd.getParentEquipmentId() + " for SubEquipment unit " + subEquipmentUnitAdd
          .getSubEquipmentId() + " is unknown");
      changeReport.setState(CHANGE_STATE.FAIL);
      return changeReport;
    }

    // Check if a SubEquipment unit with same id is not already registered
    if (parentEquipmentConfiguration.getSubEquipmentConfiguration(subEquipmentUnitAdd.getSubEquipmentId()) != null) {
      changeReport.appendError("onSubEquipmentUnitAdd - SubEquipment unit id: " + subEquipmentUnitAdd.getSubEquipmentId() + " is already registered");
      changeReport.setState(CHANGE_STATE.FAIL);
      return changeReport;
    }

    SubEquipmentConfiguration subEquipmentConfiguration = null;

    // Create the configuration
    try {
      subEquipmentConfiguration = equipmentConfigurationFactory.createSubEquipmentConfiguration(subEquipmentUnitAdd.getSubEquipmentUnitXml());
    } catch (Exception e) {
      changeReport.setState(CHANGE_STATE.FAIL);
      changeReport.appendError(StackTraceHelper.getStackTrace(e));
      return changeReport;
    }

    // Add the configuration to the parent Equipment
    parentEquipmentConfiguration.addSubEquipmentConfiguration(subEquipmentConfiguration);

    return changeReport;
  }

  /**
   * Gets the source command tags for a provided equipment id.
   *
   * @param equipmentId The equipment id to get the source command tags.
   *
   * @return The SourceCommandTags or null if the equipment does not exist.
   */
  private Map<Long, SourceCommandTag> getSourceCommandTags(final Long equipmentId) {
    ProcessConfiguration configuration = ProcessConfigurationHolder.getInstance();
    Map<Long, EquipmentConfiguration> equipmentConfigurations = configuration.getEquipmentConfigurations();
    EquipmentConfiguration equipmentConfiguration = equipmentConfigurations.get(equipmentId);
    Map<Long, SourceCommandTag> sourceCommandTags;
    if (equipmentConfiguration == null) {
      sourceCommandTags = null;
    } else {
      sourceCommandTags = equipmentConfiguration.getCommandTags();
    }
    return sourceCommandTags;
  }

  /**
   * Gets the source data tags for a provided equipment id.
   *
   * @param equipmentId The equipment id to get the source command tags.
   *
   * @return The SourceDataTags or null if the equipment does not exist.
   */
  private Map<Long, SourceDataTag> getSourceDataTags(final Long equipmentId) {
    ProcessConfiguration processConfiguration = ProcessConfigurationHolder.getInstance();
    Map<Long, EquipmentConfiguration> equipmentConfigurations = processConfiguration.getEquipmentConfigurations();
    Map<Long, SourceDataTag> sourceDataTags = null;

    EquipmentConfiguration equipmentConfiguration = equipmentConfigurations.get(equipmentId);
    if (equipmentConfiguration != null) {
      sourceDataTags = equipmentConfiguration.getDataTags();
    } else {
      // Try to find a SubEquipment that matches the given equipment ID
      for (EquipmentConfiguration configuration : processConfiguration.getEquipmentConfigurations().values()) {
        if (configuration.getSubEquipmentConfigurations().containsKey(equipmentId)) {
          log.debug("Getting source data tags of equipment " + configuration.getId() + " which is parent of SubEquipment " + equipmentId);
          sourceDataTags = configuration.getDataTags();
        }
      }
    }

    return sourceDataTags;
  }

  /**
   * Sets the process configuration of this DAQ process.
   *
   * @param processConfiguration The process configuration object.
   */
  public void setProcessConfiguration(final ProcessConfiguration processConfiguration) {
    ProcessConfigurationHolder.setInstance(processConfiguration);
  }

  /**
   * Gets the process configuration of this DAQ process.
   *
   * @return The process configuration object.
   */
  public ProcessConfiguration getProcessConfiguration() {
    return ProcessConfigurationHolder.getInstance();
  }

  /**
   * Sets the process Configuration loader of this object.
   *
   * @param processConfigurationLoader The process configuration loader.
   */
  public void setProcessConfigurationLoader(final ProcessConfigurationLoader processConfigurationLoader) {
    this.processConfigurationLoader = processConfigurationLoader;
  }

  /**
   * Adds a core data tag changer to the configuration controller. This changers
   * should never fail to maintain a proper state of the DAQ.
   *
   * @param equipmentId    The equipment id to add the changer to.
   * @param dataTagChanger The changer to add.
   */
  public void addCoreDataTagChanger(final Long equipmentId, final ICoreDataTagChanger dataTagChanger) {
    List<ICoreDataTagChanger> changers = coreDataTagChangers.get(equipmentId);
    if (changers == null) {
      changers = new ArrayList<>();
      coreDataTagChangers.put(equipmentId, changers);
    }
    changers.add(dataTagChanger);
  }

  /**
   * Adds a core command tag changer to the configuration controller. This
   * changers should never fail to maintain a proper state of the DAQ.
   *
   * @param equipmentId       The equipment id to add the changer to.
   * @param commandTagChanger The changer to add.
   */
  public void addCoreCommandTagChanger(final Long equipmentId, final ICoreCommandTagChanger commandTagChanger) {
    List<ICoreCommandTagChanger> changers = coreCommandTagChangers.get(equipmentId);
    if (changers == null) {
      changers = new ArrayList<>();
      coreCommandTagChangers.put(equipmentId, changers);
    }
    changers.add(commandTagChanger);
  }

  /**
   * Adds a core equipment configuration changer to the controller.
   *
   * @param equipmentId                       The equipment id to add the controller to.
   * @param coreEquipmentConfigurationChanger The changer to add.
   */
  public void addCoreEquipmentConfigurationChanger(final long equipmentId, final ICoreEquipmentConfigurationChanger coreEquipmentConfigurationChanger) {
    List<ICoreEquipmentConfigurationChanger> changers = coreEquipmentConfigurationChangers.get(equipmentId);
    if (changers == null) {
      changers = new ArrayList<>();
      coreEquipmentConfigurationChangers.put(equipmentId, changers);
    }
    changers.add(coreEquipmentConfigurationChanger);
  }

  /**
   * Puts an implementation command tag changer to this controller. There can
   * only be one per equipment.
   *
   * @param equipmentId       The equipment id to add the changer to.
   * @param commandTagChanger The changer to add.
   */
  public void putImplementationCommandTagChanger(final long equipmentId, final ICommandTagChanger commandTagChanger) {
    // if null is passed, remove the existing changer for given equipment (if
    // exist)
    if (commandTagChanger == null) commandTagChangers.remove(equipmentId);
    else commandTagChangers.put(equipmentId, commandTagChanger);
  }

  /**
   * Puts an implementation data tag changer to this controller. There can only
   * be one per equipment.
   *
   * @param equipmentId    The equipment id to add the changer to.
   * @param dataTagChanger The changer to add.
   */
  public void putImplementationDataTagChanger(final long equipmentId, final IDataTagChanger dataTagChanger) {
    // if null is passed, remove the existing changer for given equipment (if
    // exist)
    if (dataTagChanger == null) dataTagChangers.remove(equipmentId);
    else dataTagChangers.put(equipmentId, dataTagChanger);
  }

  /**
   * Puts an implementation equipment configuration changer to this controller.
   * There can only be one per equipment.
   *
   * @param equipmentId                   The equipment id to add the changer to.
   * @param equipmentConfigurationChanger The changer to add.
   */
  public void putImplementationEquipmentConfigurationChanger(final long equipmentId, final IEquipmentConfigurationChanger equipmentConfigurationChanger) {
    equipmentChangers.put(equipmentId, equipmentConfigurationChanger);
  }

  /**
   * Gets the equipment configuration.
   *
   * @param equipmentId The id of the equipment configuration.
   *
   * @return The equipment configuration with the provided id or null if there
   * is none.
   */
  public EquipmentConfiguration getEquipmentConfiguration(final Long equipmentId) {
    return getProcessConfiguration().getEquipmentConfiguration(equipmentId);
  }

  /**
   * Searches a data tag with the provided id and returns the first found.
   *
   * @param dataTagId The data tag id to search for.
   *
   * @return The first found data tag or null if none is found.
   */
  public ISourceDataTag findDataTag(final Long dataTagId) {
    Map<Long, EquipmentConfiguration> equipmentMap = getProcessConfiguration().getEquipmentConfigurations();
    for (EquipmentConfiguration equipmentConfiguration : equipmentMap.values()) {
      if (equipmentConfiguration.hasSourceDataTag(dataTagId)) {
        return equipmentConfiguration.getSourceDataTag(dataTagId);
      }
    }
    return null;
  }

  /**
   * Searches a command tag with the provided id and returns the first found.
   *
   * @param commandTagId The command tag id to search for.
   *
   * @return The first found command tag or null if none is found.
   */
  public ISourceCommandTag findCommandTag(final Long commandTagId) {
    Map<Long, EquipmentConfiguration> equipmentMap = getProcessConfiguration().getEquipmentConfigurations();
    for (EquipmentConfiguration equipmentConfiguration : equipmentMap.values()) {
      if (equipmentConfiguration.hasSourceCommandTag(commandTagId)) {
        return equipmentConfiguration.getSourceCommandTag(commandTagId);
      }
    }
    return null;
  }

  /**
   * This method sets the startup time of the process (in milliseconds)
   *
   * @param pStartUp time in milliseconds
   */
  public final void setStartUp(final long pStartUp) {
    startUp = pStartUp;
  }

  /**
   * This method gets the startup time of the process (in milliseconds)
   *
   * @return long
   */
  public long getStartUp() {
    return startUp;
  }

	/**
	 * Set the equipmentConfigurationFactory.
	 *
	 * @param equipmentConfigurationFactory
	 */
	public void setEquipmentConfigurationFactory(EquipmentConfigurationFactory equipmentConfigurationFactory) {
		this.equipmentConfigurationFactory = equipmentConfigurationFactory;
	}
}