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

package de.rcenvironment.components.switchcmp.execution;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.apache.commons.lang3.text.WordUtils;

import de.rcenvironment.components.switchcmp.common.ScriptValidation;
import de.rcenvironment.components.switchcmp.common.SwitchComponentConstants;
import de.rcenvironment.components.switchcmp.common.SwitchComponentHistoryDataItem;
import de.rcenvironment.core.component.api.ComponentConstants;
import de.rcenvironment.core.component.api.ComponentException;
import de.rcenvironment.core.component.execution.api.ComponentContext;
import de.rcenvironment.core.component.model.spi.DefaultComponent;
import de.rcenvironment.core.datamodel.api.DataType;
import de.rcenvironment.core.datamodel.api.TypedDatum;
import de.rcenvironment.core.scripting.ScriptingService;
import de.rcenvironment.core.scripting.ScriptingUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.scripting.ScriptLanguage;

/**
 * Switch component.
 * 
 * @author David Scholz
 * @author Doreen Seider
 */
public class SwitchComponent extends DefaultComponent {

    protected static ScriptingService scriptingService;

    private static final String QUOTE = "\\\\Q";

    private static final String ENDQUOTE = "\\\\E";

    private ScriptLanguage scriptLanguage;

    private ScriptEngine engine;

    private String condition;
    
    private CloseOutputBehavior closeOutputBehavior;

    private ComponentContext componentContext;

    private SwitchComponentHistoryDataItem historyDataItem;
    
    /**
     * Whether outputs should be closed.
     * 
     * @author Doreen Seider
     */
    private enum CloseOutputBehavior {
        NeverCloseOutputs,
        CloseOutputsOnTrue,
        CloseOutputsOnFalse;
    }

    @Override
    public void setComponentContext(ComponentContext componentContext) {
        this.componentContext = componentContext;
    }

    @Override
    public void start() throws ComponentException {
        condition = componentContext.getConfigurationValue(SwitchComponentConstants.CONDITION_KEY);
        closeOutputBehavior = getCloseOutputBehavior();
        scriptLanguage = ScriptLanguage.getByName(SwitchComponentConstants.SCRIPT_LANGUAGE);
        scriptingService = componentContext.getService(ScriptingService.class);
        engine = scriptingService.createScriptEngine(scriptLanguage);
        String errorMessage = ScriptValidation.validateScript(condition, engine, getInputAndConnectionStatus(),
                getInputsAndDataTypes());
        if (!errorMessage.isEmpty()) { // validation before workflowstart
            throw new ComponentException(errorMessage);
        }
    }

    // Use one key for each enum value in component config map to make use of the property handling in the GUI provided by
    // WorkflowNodePropertySection. It can't map a radio button group to enums. (But it can map one single radio button to one config key.
    private CloseOutputBehavior getCloseOutputBehavior() {
        if (Boolean.valueOf(componentContext.getConfigurationValue(SwitchComponentConstants.CLOSE_OUTPUTS_ON_TRUE_KEY))) {
            return CloseOutputBehavior.CloseOutputsOnTrue;
        } else if (Boolean.valueOf(componentContext.getConfigurationValue(SwitchComponentConstants.CLOSE_OUTPUTS_ON_FALSE_KEY))) {
            return CloseOutputBehavior.CloseOutputsOnFalse;
        } else {
            return CloseOutputBehavior.NeverCloseOutputs;
        }
    }

    @Override
    public void processInputs() throws ComponentException {
        initializeNewHistoryDataItem();
        String conditionWithActualValues = condition;
        TypedDatum switchDatum = componentContext.readInput(SwitchComponentConstants.DATA_INPUT_NAME);
        for (String inputName : componentContext.getInputsWithDatum()) {
            TypedDatum datum = componentContext.readInput(inputName);
            if (datum.getDataType().equals(DataType.Float) || datum.getDataType().equals(DataType.Integer)) {
                conditionWithActualValues = conditionWithActualValues.replace(inputName, Pattern.quote(datum.toString())
                    .replaceAll(QUOTE, "")).replaceAll(ENDQUOTE, "");
            }
            if (datum.getDataType().equals(DataType.Boolean)) {
                conditionWithActualValues =
                    conditionWithActualValues.replace(inputName, WordUtils.capitalize(Pattern.quote(datum.toString())
                        .replaceAll(QUOTE, ""))).replaceAll(ENDQUOTE, "");
            }
        }

        String evalScript = "if " + conditionWithActualValues + ":\n    returnValue=True\nelse:\n    returnValue=False";
        Object returnValue;
        try {
            // As the Jython script engine is not thread safe (console outputs of multiple script
            // executions are mixed), we must ensure that at most one script is executed at the same
            // time
            synchronized (ScriptingUtils.SCRIPT_EVAL_LOCK_OBJECT) {
                engine.eval(evalScript);
                returnValue = engine.get("returnValue");
            }
        } catch (ScriptException e) {
            throw new ComponentException(StringUtils.format("Failed to interpret condition '%s': %s", 
                conditionWithActualValues, e.getMessage()), e); // should never happen
        }
        
        componentContext.getLog().componentInfo(StringUtils.format("Evaluated '%s' -> %b", conditionWithActualValues, returnValue));

        if (historyDataItem != null) {
            historyDataItem.setActualCondition(StringUtils.format("%s -> %b", conditionWithActualValues, returnValue));
            historyDataItem.setConditionPattern(condition);
        }

        String outputName;
        if ((Boolean) returnValue) {
            componentContext.writeOutput(SwitchComponentConstants.TRUE_OUTPUT, switchDatum);
            if (closeOutputBehavior == CloseOutputBehavior.CloseOutputsOnTrue) {
                componentContext.closeAllOutputs();
            }
            outputName = SwitchComponentConstants.TRUE_OUTPUT;
        } else {
            componentContext.writeOutput(SwitchComponentConstants.FALSE_OUTPUT, switchDatum);
            if (closeOutputBehavior == CloseOutputBehavior.CloseOutputsOnFalse) {
                componentContext.closeAllOutputs();
            }
            outputName = SwitchComponentConstants.FALSE_OUTPUT;
        }
        componentContext.getLog().componentInfo(StringUtils.format("Wrote to '%s': %s", outputName, switchDatum));

        writeFinalHistoryDataItem();
    }
    
    @Override
    public void completeStartOrProcessInputsAfterFailure() throws ComponentException {
        writeFinalHistoryDataItem();
    }

    private void initializeNewHistoryDataItem() {
        if (Boolean.valueOf(componentContext.getConfigurationValue(ComponentConstants.CONFIG_KEY_STORE_DATA_ITEM))) {
            historyDataItem = new SwitchComponentHistoryDataItem(SwitchComponentConstants.COMPONENT_ID);
        }
    }

    private void writeFinalHistoryDataItem() {
        if (Boolean.valueOf(componentContext.getConfigurationValue(ComponentConstants.CONFIG_KEY_STORE_DATA_ITEM))) {
            componentContext.writeFinalHistoryDataItem(historyDataItem);
        }
    }

    private Map<String, Boolean> getInputAndConnectionStatus() {
        Map<String, Boolean> inputs = new HashMap<>();
        
        for (String name : componentContext.getInputs()) {
            inputs.put(name, true);
        }
        
        for (String name : componentContext.getInputsNotConnected()) {
            inputs.put(name, false);
        }

        return inputs;
    }
    
    private Map<String, DataType> getInputsAndDataTypes() {
        Map<String, DataType> inputs = new HashMap<>();
        
        for (String name : componentContext.getInputs()) {
            inputs.put(name, componentContext.getInputDataType(name));
        }
        for (String name : componentContext.getInputsNotConnected()) {
            inputs.put(name, componentContext.getInputDataType(name));
        }
        return inputs;
    }

}
