/*
 * Copyright 2006-2019 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.components.optimizer.gui.properties.commands;

import java.util.HashMap;
import java.util.Map;

import de.rcenvironment.components.optimizer.common.OptimizerComponentConstants;
import de.rcenvironment.core.component.api.ComponentConstants;
import de.rcenvironment.core.component.model.endpoint.api.EndpointDefinition;
import de.rcenvironment.core.component.model.endpoint.api.EndpointDescription;
import de.rcenvironment.core.component.workflow.model.api.WorkflowNode;
import de.rcenvironment.core.datamodel.api.EndpointType;
import de.rcenvironment.core.gui.workflow.editor.commands.endpoint.EditDynamicEndpointCommand;
import de.rcenvironment.core.gui.workflow.editor.properties.Refreshable;

/**
 * Command for editing an endpoint in the optimizer component.
 * 
 * @author Sascha Zur
 */
public class OptimizerEditDynamicEndpointCommand extends EditDynamicEndpointCommand {

    private static final String DESIGN_VARIABLE = "Design";

    private static final String DASH = "-";

    public OptimizerEditDynamicEndpointCommand(EndpointType direction, String id, EndpointDescription oldDescription,
        EndpointDescription newDescription, boolean executable, boolean undoable, Refreshable... refreshable) {
        super(direction, oldDescription, newDescription, refreshable);
    }

    @Override
    public void execute() {
        final WorkflowNode workflowNode = getWorkflowNode();
        super.execute();

        switch (direction) {
        case INPUT:
            if (hasGradient(oldDesc)) {
                if (hasGradient(newDesc)) {
                    executeBothHaveGradients(workflowNode);
                } else {
                    for (EndpointDescription variable : workflowNode.getOutputDescriptionsManager().getDynamicEndpointDescriptions()) {
                        if (variable.getDynamicEndpointIdentifier().equals(DESIGN_VARIABLE)) {
                            String oldGradientName =
                                OptimizerDynamicEndpointCommandHelper.createGradientChannelName(oldDesc.getName(), variable.getName());
                            workflowNode.getInputDescriptionsManager().removeDynamicEndpointDescription(oldGradientName);
                        }
                    }
                }

            } else {
                if (hasGradient(newDesc)) {
                    for (EndpointDescription variable : workflowNode.getOutputDescriptionsManager().getDynamicEndpointDescriptions()) {
                        if (variable.getDynamicEndpointIdentifier().equals(DESIGN_VARIABLE)) {
                            String newGradientName =
                                OptimizerDynamicEndpointCommandHelper.createGradientChannelName(newDesc.getName(), variable.getName());
                            Map<String, String> metaData = new HashMap<>();
                            for (String key : newDesc.getMetaData().keySet()) {
                                if (key.equals(OptimizerComponentConstants.METADATA_VECTOR_SIZE)
                                    || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_DATUM_HANDLING)
                                    || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT)) {
                                    metaData.put(key, newDesc.getMetaData().get(key));
                                } else {
                                    metaData.put(key, DASH);
                                }
                            }

                            workflowNode.getInputDescriptionsManager().addDynamicEndpointDescription(
                                OptimizerComponentConstants.ID_GRADIENTS, newGradientName,
                                variable.getDataType(), metaData);
                        }
                    }
                }
            }

            break;
        case OUTPUT:
            if (!newDesc.getName().equals(oldDesc.getName()) || !newDesc.getDataType().equals(oldDesc.getDataType())) {
                for (EndpointDescription function : workflowNode.getInputDescriptionsManager().getDynamicEndpointDescriptions()) {
                    if (!workflowNode.getInputDescriptionsManager().isValidEndpointName(
                        OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), oldDesc.getName()))) {
                        String oldName =
                            OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), oldDesc.getName());
                        String newName =
                            OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), newDesc.getName());
                        EndpointDescription desc = workflowNode.getInputDescriptionsManager().getEndpointDescription(
                            OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), oldDesc.getName()));
                        desc.setName(newName);
                        workflowNode.getInputDescriptionsManager().editDynamicEndpointDescription(oldName, newName, newDesc.getDataType(),
                            desc.getMetaData());
                    }
                }

            }

            if (!oldDesc.getName().equals(newDesc.getName()) || !oldDesc.getDataType().equals(newDesc.getDataType())) {
                if (getWorkflowNode().getOutputDescriptionsManager().getEndpointDescription(
                    oldDesc.getName() + OptimizerComponentConstants.OPTIMUM_VARIABLE_SUFFIX) != null) {
                    getWorkflowNode().getOutputDescriptionsManager().removeDynamicEndpointDescription(
                        oldDesc.getName() + OptimizerComponentConstants.OPTIMUM_VARIABLE_SUFFIX);
                }
                Map<String, String> metadata = new HashMap<>();
                metadata.putAll(newDesc.getMetaData());
                getWorkflowNode().getOutputDescriptionsManager().addDynamicEndpointDescription(OptimizerComponentConstants.ID_OPTIMA,
                    newDesc.getName() + OptimizerComponentConstants.OPTIMUM_VARIABLE_SUFFIX,
                    newDesc.getDataType(), metadata);
            }
            handleExtraValueInputs(workflowNode, OptimizerComponentConstants.META_HAS_STARTVALUE,
                null, OptimizerComponentConstants.STARTVALUE_SIGNATURE, true, oldDesc, newDesc);
            handleExtraValueInputs(workflowNode, OptimizerComponentConstants.META_USE_STEP,
                OptimizerComponentConstants.META_USE_UNIFIED_STEP, OptimizerComponentConstants.STEP_VALUE_SIGNATURE, false, oldDesc,
                newDesc);

            break;
        default:
            throw new RuntimeException();
        }
        String oldHasBounds = oldDesc.getMetaDataValue(OptimizerComponentConstants.META_KEY_HAS_BOUNDS);
        String newHasBounds = newDesc.getMetaDataValue(OptimizerComponentConstants.META_KEY_HAS_BOUNDS);
        if (oldHasBounds != null && Boolean.parseBoolean(oldHasBounds)) {
            if (!Boolean.parseBoolean(newHasBounds)) {
                OptimizerDynamicEndpointCommandHelper.addLowerAndUpperBoundsEndpoints(newDesc.getName(), newDesc.getDataType(),
                    newDesc.getMetaData(), workflowNode);
            }
        }
        if (oldHasBounds != null && !Boolean.parseBoolean(oldHasBounds)) {
            if (Boolean.parseBoolean(newHasBounds)) {
                OptimizerDynamicEndpointCommandHelper.removeUpperLowerBoundsEndpoints(oldDesc.getName(), oldDesc.getMetaData(),
                    workflowNode);
            } else {
                if (!oldDesc.getName().equals(newDesc.getName())) {
                    OptimizerDynamicEndpointCommandHelper.removeUpperLowerBoundsEndpoints(oldDesc.getName(), oldDesc.getMetaData(),
                        workflowNode);
                    OptimizerDynamicEndpointCommandHelper.addLowerAndUpperBoundsEndpoints(newDesc.getName(), newDesc.getDataType(),
                        newDesc.getMetaData(), workflowNode);
                }
            }
        }
        if (refreshable != null) {
            for (Refreshable r : refreshable) {
                r.refresh();
            }
        }
    }

    private void handleExtraValueInputs(final WorkflowNode workflowNode, final String conditionName, final String condition2Name,
        final String signature, boolean negateCondition, EndpointDescription fromDescription, EndpointDescription toDescription) {

        String newConditionValueString = toDescription.getMetaDataValue(conditionName);
        boolean newConditionValue = newConditionValueString != null && Boolean.parseBoolean(newConditionValueString);

        String oldConditionValueString = fromDescription.getMetaDataValue(conditionName);
        boolean oldConditionValue = oldConditionValueString != null && Boolean.parseBoolean(oldConditionValueString);

        boolean condition2Active = false;
        boolean newCondition2Value = false;
        boolean oldCondition2Value = false;
        if (condition2Name != null) {
            condition2Active = true;
            String newCondition2ValueString = toDescription.getMetaDataValue(condition2Name);
            newCondition2Value = newCondition2ValueString != null && !Boolean.parseBoolean(newCondition2ValueString);
            String oldCondition2ValueString = fromDescription.getMetaDataValue(condition2Name);
            oldCondition2Value = oldCondition2ValueString != null && !Boolean.parseBoolean(oldCondition2ValueString);
        }

        boolean nameChanged = !fromDescription.getName().equals(toDescription.getName());
        boolean dataTypeChanged = !fromDescription.getDataType().equals(toDescription.getDataType());

        if (negateCondition) {
            newConditionValue = !newConditionValue;
            oldConditionValue = !oldConditionValue;
        }

        if ((nameChanged || dataTypeChanged) && oldConditionValue && (!condition2Active || (oldCondition2Value))) {
            workflowNode.getInputDescriptionsManager().editDynamicEndpointDescription(fromDescription.getName() + signature,
                toDescription.getName() + signature, toDescription.getDataType(), fromDescription.getMetaData());
        }
        if (oldConditionValue && newConditionValue && condition2Active) {
            if (oldCondition2Value && !newCondition2Value) {
                if (workflowNode.getInputDescriptionsManager().getEndpointDescription(toDescription.getName() + signature) != null) {
                    workflowNode.getInputDescriptionsManager().removeDynamicEndpointDescription(
                        toDescription.getName() + signature);
                }
            }
            if (!oldCondition2Value && newCondition2Value) {
                Map<String, String> metaData = new HashMap<>();
                metaData.put(ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT,
                    EndpointDefinition.InputExecutionContraint.Required.name());
                workflowNode.getInputDescriptionsManager().addDynamicEndpointDescription(OptimizerComponentConstants.ID_STARTVALUES,
                    toDescription.getName() + signature, toDescription.getDataType(), metaData);
            }
        }
        if (oldConditionValue && !newConditionValue
            && (!condition2Active || (oldCondition2Value && !newCondition2Value))) {
            if (workflowNode.getInputDescriptionsManager().getEndpointDescription(toDescription.getName() + signature) != null) {
                workflowNode.getInputDescriptionsManager().removeDynamicEndpointDescription(
                    toDescription.getName() + signature);
            }
        }

        if (!oldConditionValue && newConditionValue) {
            if (!condition2Active || (!oldCondition2Value && newCondition2Value)) {
                Map<String, String> metaData = new HashMap<>();
                metaData.put(ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT,
                    EndpointDefinition.InputExecutionContraint.Required.name());
                workflowNode.getInputDescriptionsManager().addDynamicEndpointDescription(OptimizerComponentConstants.ID_STARTVALUES,
                    toDescription.getName() + signature, toDescription.getDataType(), metaData);
            }
        }
    }

    private void executeBothHaveGradients(final WorkflowNode workflowNode) {
        if (!oldDesc.getName().equals(newDesc.getName()) || oldDesc.getDataType() != newDesc.getDataType()) {
            for (EndpointDescription variable : workflowNode.getOutputDescriptionsManager().getDynamicEndpointDescriptions()) {
                if (!variable.getName().contains(OptimizerComponentConstants.OPTIMUM_VARIABLE_SUFFIX)) {
                    String oldGradientName =
                        OptimizerDynamicEndpointCommandHelper.createGradientChannelName(oldDesc.getName(), variable.getName());
                    String newGradientName =
                        OptimizerDynamicEndpointCommandHelper.createGradientChannelName(newDesc.getName(), variable.getName());
                    EndpointDescription newDesc =
                        workflowNode.getInputDescriptionsManager().getEndpointDescription(oldGradientName);
                    newDesc.setName(newGradientName);
                    newDesc.setDataType(this.newDesc.getDataType());

                    Map<String, String> gradientMetadata = new HashMap<>();
                    for (String key : this.newDesc.getMetaData().keySet()) {
                        if (key.equals(OptimizerComponentConstants.METADATA_VECTOR_SIZE)
                            || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_DATUM_HANDLING)
                            || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT)) {
                            gradientMetadata.put(key, this.newDesc.getMetaData().get(key));
                        } else {
                            gradientMetadata.put(key, DASH);
                        }
                    }
                    newDesc.setMetaData(gradientMetadata);
                    workflowNode.getInputDescriptionsManager().editDynamicEndpointDescription(oldGradientName, newGradientName,
                        newDesc.getDataType(), newDesc.getMetaData());
                }
            }
        }
    }

    private boolean hasGradient(EndpointDescription description) {
        if (description.getMetaDataValue(OptimizerComponentConstants.HAS_GRADIENT) != null
            && Boolean.parseBoolean(description.getMetaDataValue(OptimizerComponentConstants.HAS_GRADIENT))) {
            return true;
        }
        return false;
    }

    @Override
    public void undo() {
        final WorkflowNode workflowNode = getWorkflowNode();
        super.undo();
        switch (direction) {
        case INPUT:
            if (hasGradient(newDesc)) {
                if (hasGradient(oldDesc)) {
                    undoBothHaveGradients(workflowNode);
                } else {
                    for (EndpointDescription variable : workflowNode.getOutputDescriptionsManager().getDynamicEndpointDescriptions()) {
                        if (variable.getDynamicEndpointIdentifier().equals(DESIGN_VARIABLE)) {
                            String newGradientName =
                                OptimizerDynamicEndpointCommandHelper.createGradientChannelName(newDesc.getName(), variable.getName());
                            workflowNode.getInputDescriptionsManager().removeDynamicEndpointDescription(newGradientName);
                        }
                    }
                }
            } else {
                if (hasGradient(oldDesc)) {
                    for (EndpointDescription variable : workflowNode.getOutputDescriptionsManager().getDynamicEndpointDescriptions()) {
                        if (variable.getDynamicEndpointIdentifier().equals(DESIGN_VARIABLE)) {
                            String oldGradientName =
                                OptimizerDynamicEndpointCommandHelper.createGradientChannelName(oldDesc.getName(), variable.getName());
                            Map<String, String> metaData = new HashMap<>();
                            for (String key : newDesc.getMetaData().keySet()) {
                                if (key.equals(OptimizerComponentConstants.METADATA_VECTOR_SIZE)
                                    || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_DATUM_HANDLING)
                                    || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT)) {
                                    metaData.put(key, newDesc.getMetaData().get(key));
                                } else {
                                    metaData.put(key, DASH);
                                }
                            }
                            workflowNode.getInputDescriptionsManager().addDynamicEndpointDescription(
                                OptimizerComponentConstants.ID_GRADIENTS, oldGradientName,
                                oldDesc.getDataType(), metaData);
                        }
                    }
                }
            }
            break;
        case OUTPUT:
            if (!newDesc.getName().equals(oldDesc.getName())) {
                for (EndpointDescription function : workflowNode.getInputDescriptionsManager().getDynamicEndpointDescriptions()) {
                    if (!workflowNode.getInputDescriptionsManager().isValidEndpointName(
                        OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), newDesc.getName()))) {
                        String oldName =
                            OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), oldDesc.getName());
                        String newName =
                            OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), newDesc.getName());
                        EndpointDescription desc = workflowNode.getInputDescriptionsManager().getEndpointDescription(
                            OptimizerDynamicEndpointCommandHelper.createGradientChannelName(function.getName(), newDesc.getName()));
                        desc.setName(oldName);
                        workflowNode.getInputDescriptionsManager().editDynamicEndpointDescription(newName, oldName, desc.getDataType(),
                            desc.getMetaData());
                    }
                }
                if (getWorkflowNode().getOutputDescriptionsManager().getEndpointDescription(
                    newDesc.getName() + OptimizerComponentConstants.OPTIMUM_VARIABLE_SUFFIX) != null) {
                    getWorkflowNode().getOutputDescriptionsManager().removeDynamicEndpointDescription(
                        newDesc.getName() + OptimizerComponentConstants.OPTIMUM_VARIABLE_SUFFIX);
                }
                Map<String, String> metadata = oldDesc.getMetaData();
                getWorkflowNode().getOutputDescriptionsManager().addDynamicEndpointDescription(OptimizerComponentConstants.ID_OPTIMA,
                    oldDesc.getName() + OptimizerComponentConstants.OPTIMUM_VARIABLE_SUFFIX,
                    newDesc.getDataType(), metadata);
            }
            handleExtraValueInputs(workflowNode, OptimizerComponentConstants.META_HAS_STARTVALUE,
                null, OptimizerComponentConstants.STARTVALUE_SIGNATURE, true, newDesc, oldDesc);
            handleExtraValueInputs(workflowNode, OptimizerComponentConstants.META_USE_STEP,
                OptimizerComponentConstants.META_USE_UNIFIED_STEP, OptimizerComponentConstants.STEP_VALUE_SIGNATURE, false, newDesc,
                oldDesc);
            break;
        default:
            throw new RuntimeException();
        }
        String oldHasBounds = oldDesc.getMetaDataValue(OptimizerComponentConstants.META_KEY_HAS_BOUNDS);
        String newHasBounds = newDesc.getMetaDataValue(OptimizerComponentConstants.META_KEY_HAS_BOUNDS);

        if (Boolean.parseBoolean(newHasBounds)) {
            if (oldHasBounds != null && !Boolean.parseBoolean(oldHasBounds)) {
                OptimizerDynamicEndpointCommandHelper.addLowerAndUpperBoundsEndpoints(oldDesc.getName(), oldDesc.getDataType(),
                    oldDesc.getMetaData(), workflowNode);
            }
        } else {
            if (oldHasBounds != null && !Boolean.parseBoolean(oldHasBounds)) {
                if (!oldDesc.getName().equals(newDesc.getName())) {
                    OptimizerDynamicEndpointCommandHelper.removeUpperLowerBoundsEndpoints(newDesc.getName(), newDesc.getMetaData(),
                        workflowNode);
                    OptimizerDynamicEndpointCommandHelper.addLowerAndUpperBoundsEndpoints(oldDesc.getName(), oldDesc.getDataType(),
                        oldDesc.getMetaData(), workflowNode);
                }
            } else {
                OptimizerDynamicEndpointCommandHelper.removeUpperLowerBoundsEndpoints(newDesc.getName(), newDesc.getMetaData(),
                    workflowNode);
            }
        }
        if (refreshable != null) {
            for (Refreshable r : refreshable) {
                r.refresh();
            }
        }
    }

    private void undoBothHaveGradients(final WorkflowNode workflowNode) {
        if (!oldDesc.getName().equals(newDesc.getName()) || oldDesc.getDataType() != newDesc.getDataType()) {
            for (EndpointDescription variable : workflowNode.getOutputDescriptionsManager().getDynamicEndpointDescriptions()) {
                if (variable.getDynamicEndpointIdentifier().equals(DESIGN_VARIABLE)) {
                    String oldGradientName =
                        OptimizerDynamicEndpointCommandHelper.createGradientChannelName(oldDesc.getName(), variable.getName());
                    String newGradientName =
                        OptimizerDynamicEndpointCommandHelper.createGradientChannelName(newDesc.getName(), variable.getName());
                    EndpointDescription desc =
                        workflowNode.getInputDescriptionsManager().getEndpointDescription(newGradientName);
                    desc.setName(oldGradientName);
                    Map<String, String> gradientMetadata = new HashMap<>();
                    for (String key : this.oldDesc.getMetaData().keySet()) {
                        if (key.equals(OptimizerComponentConstants.METADATA_VECTOR_SIZE)
                            || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_DATUM_HANDLING)
                            || key.startsWith(ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT)) {
                            gradientMetadata.put(key, newDesc.getMetaData().get(key));
                        } else {
                            gradientMetadata.put(key, DASH);
                        }
                    }
                    desc.setMetaData(gradientMetadata);
                    workflowNode.getInputDescriptionsManager().editDynamicEndpointDescription(newGradientName, oldGradientName,
                        oldDesc.getDataType(), gradientMetadata);
                }
            }
        }
    }
}
