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

package de.rcenvironment.components.optimizer.common.execution;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.fasterxml.jackson.databind.ObjectMapper;

import de.rcenvironment.components.optimizer.common.MethodDescription;
import de.rcenvironment.components.optimizer.common.OptimizerComponentConstants;
import de.rcenvironment.core.component.api.ComponentException;
import de.rcenvironment.core.component.execution.api.ComponentContext;
import de.rcenvironment.core.datamodel.api.DataType;
import de.rcenvironment.core.datamodel.api.TypedDatum;
import de.rcenvironment.core.datamodel.api.TypedDatumService;
import de.rcenvironment.core.datamodel.types.api.FloatTD;
import de.rcenvironment.core.datamodel.types.api.VectorTD;
import de.rcenvironment.core.utils.common.JsonUtils;

/**
 * A part implementation for the algorithm executor which write config files in python.
 * 
 * @author Sascha Zur
 * @author Kathrin Schaffert (#17540)
 */
public abstract class CommonPythonAlgorithmExecutor extends OptimizerAlgorithmExecutor {

    protected static final Log LOGGER = LogFactory.getLog(OptimizerAlgorithmExecutor.class);

    protected static final String CLOSE_BRACKET_AND_NL = ")\n";

    protected static final String REGEX_DOT = "\\.";

    private static final String DOT = ".";

    private static final int NEGATE_VALUE = -1;

    private static final String META_BASE = "base";

    private static final String VALUE = "value";

    protected final String apostroph = "'";

    protected final String comma = ",";

    protected Map<String, TypedDatum> outputValues;

    protected Collection<String> input;

    protected String algorithm;

    protected Map<String, MethodDescription> methodConfiguration;

    protected Map<String, Double> lowerMap;

    protected Map<String, Double> upperMap;

    private boolean gradRequest;

    private List<String> orderedOutputValueKeys;

    private Map<String, Double> stepValues;

    public CommonPythonAlgorithmExecutor(Map<String, MethodDescription> methodConfiguration,
        Map<String, TypedDatum> outputValues,
        Collection<String> input, ComponentContext compContext,
        Map<String, Double> upperMap, Map<String, Double> lowerMap, Map<String, Double> stepValues, String inputFilename)
        throws ComponentException {
        super(compContext, compContext.getInstanceName(), inputFilename);
        this.algorithm = compContext.getConfigurationValue(OptimizerComponentConstants.ALGORITHMS);
        this.methodConfiguration = methodConfiguration;
        this.outputValues = outputValues;
        this.input = input;
        this.lowerMap = lowerMap;
        this.upperMap = upperMap;
        this.stepValues = stepValues;
        typedDatumFactory = compContext.getService(TypedDatumService.class).getFactory();

    }

    public CommonPythonAlgorithmExecutor() {

    }

    protected void writeConfigurationFile(File configFile) throws ComponentException {
        Map<String, Object> configuration = new HashMap<>();

        addOutputsToConfig(configuration);

        addInputsToConfig(configuration);

        String[] algos = algorithm.split(comma);

        for (String algo : algos) {
            Map<String, Map<String, String>> allSettings = methodConfiguration.get(algo).getSpecificSettings();
            Map<String, Object> settings = new HashMap<>();
            for (String key : allSettings.keySet()) {
                Map<String, Object> set = new HashMap<>();
                String dataType = allSettings.get(key).get("dataType");
                String value = allSettings.get(key).get("Value");
                if (value == null || value.isEmpty()) {
                    value = allSettings.get(key).get("DefaultValue");
                }
                switch (dataType.toLowerCase()) {
                case "real":
                    set.put(VALUE, Double.parseDouble(value));

                    break;
                case "int":
                    set.put(VALUE, Integer.parseInt(value));
                    break;
                case "bool":
                    set.put(VALUE, Boolean.parseBoolean(value));
                    break;
                case "None":
                default:
                    set.put(VALUE, value);
                }
                settings.put(key, set);
            }
            configuration.put("algorithmSettings", settings);
            configuration.put("algorithm", algo.substring(0, algo.lastIndexOf("[") - 1));
        }

        ObjectMapper mapper = JsonUtils.getDefaultObjectMapper();
        try {
            mapper.writerWithDefaultPrettyPrinter().writeValue(configFile, configuration);
        } catch (IOException e) {
            throw new ComponentException("Failed to write configuration file", e);
        }

    }

    private void addInputsToConfig(Map<String, Object> configuration) {
        List<String> orderedInputValueKeys = new ArrayList<String>(input.size());
        for (String e : input) {
            orderedInputValueKeys.add(e);
        }
        Collections.sort(orderedInputValueKeys);

        List<String> orderedObjectives = new LinkedList<>();
        List<String> orderedConstraints = new LinkedList<>();
        List<String> orderedGradients = new LinkedList<>();
        Map<String, Double> minValues = new HashMap<>();
        Map<String, Double> maxValues = new HashMap<>();
        Map<String, Double> objectiveWeights = new HashMap<>();
        Map<String, String> optimizationTargets = new HashMap<>();

        for (String key : orderedInputValueKeys) {
            if (compContext.getDynamicInputIdentifier(key).equals(OptimizerComponentConstants.ID_OBJECTIVE)
                && !key.contains(OptimizerComponentConstants.GRADIENT_DELTA)) {
                orderedObjectives.add(key);
                String weight = compContext.getInputMetaDataValue(key, OptimizerComponentConstants.META_WEIGHT);
                objectiveWeights.put(key, Double.parseDouble(weight));
                String target = compContext.getInputMetaDataValue(key, OptimizerComponentConstants.META_GOAL);
                optimizationTargets.put(key, target);
            }
            if (compContext.getDynamicInputIdentifier(key).equals(OptimizerComponentConstants.ID_CONSTRAINT)
                && !key.contains(OptimizerComponentConstants.GRADIENT_DELTA)) {
                orderedConstraints.add(key);
                minValues.put(key, lowerMap.get(key));
                maxValues.put(key, upperMap.get(key));
            }
            if (key.contains(OptimizerComponentConstants.GRADIENT_DELTA)) {
                orderedGradients.add(key);
            }
        }

        configuration.put("objectives", orderedObjectives);
        configuration.put("constraints", orderedConstraints);
        configuration.put("gradients", orderedGradients);
        configuration.put("objectivesWeights", objectiveWeights);
        configuration.put("optimizationTargets", optimizationTargets);
        configuration.put("minValuesConstraints", minValues);
        configuration.put("maxValuesConstraints", maxValues);

    }

    private void addOutputsToConfig(Map<String, Object> configuration) {
        orderedOutputValueKeys = new ArrayList<String>();
        for (String output : outputValues.keySet()) {
            if (compContext.getOutputDataType(output) == DataType.Vector) {
                for (int i = 0; i < Integer.valueOf(compContext.getOutputMetaDataValue(output,
                    OptimizerComponentConstants.METADATA_VECTOR_SIZE)); i++) {
                    orderedOutputValueKeys.add(output + OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL + i);
                }
            } else {
                orderedOutputValueKeys.add(output);
            }
        }
        Collections.sort(orderedOutputValueKeys);
        Map<String, Double> initValues = new HashMap<>();
        Map<String, Double> baseValues = new HashMap<>();
        // Map<String, Double> stepValues = new HashMap<>();
        Map<String, Boolean> discreteValues = new HashMap<>();
        Map<String, Double> minValues = new HashMap<>();
        Map<String, Double> maxValues = new HashMap<>();

        for (String key : orderedOutputValueKeys) {

            if (key.contains(OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL)
                && compContext.getOutputDataType(getVectorName(key)) == DataType.Vector) {
                if (outputValues.containsKey(key)) {
                    initValues.put(key, ((FloatTD) outputValues.get(key)).getFloatValue());
                }
                discreteValues.put(key, Boolean.parseBoolean(
                    compContext.getOutputMetaDataValue(getVectorName(key), OptimizerComponentConstants.META_IS_DISCRETE)));
            } else {
                initValues.put(key, ((FloatTD) outputValues.get(key)).getFloatValue());
            }
            // The META_BASE = "base" configuration value became obsolete in the meantime. I couldn't find any other code reference to such
            // a value, so I don't know where it might be set.
            // In addition, this parameter is not defined in the Optimizer's output.json, so it can never be set via the GUI.
            // Therefore the following code lines (compContext.getOutputMetaDataValue(getVectorName(key), META_BASE) != null) and
            // (compContext.getOutputMetaDataValue(key, META_BASE) != null) will never be true.
            // For backwards compatibility, the following code that references this value has not yet been removed.
            // 14.10.2022, Kathrin Schaffert
            if (!key.contains(OptimizerComponentConstants.GRADIENT_DELTA)) {
                if (key.contains(OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL)) {
                    if (compContext.getOutputMetaDataValue(getVectorName(key), META_BASE) != null) {
                        baseValues.put(key, Double.valueOf(compContext.getOutputMetaDataValue(
                            getVectorName(key), META_BASE)));
                    }
                } else {
                    if (compContext.getOutputMetaDataValue(key, META_BASE) != null) {
                        baseValues.put(key, Double.valueOf(compContext.getOutputMetaDataValue(key, META_BASE)));
                    }
                }
            }
            minValues.put(key, lowerMap.get(key));
            maxValues.put(key, upperMap.get(key));
        }

        configuration.put("designVariableCount", orderedOutputValueKeys.size());
        configuration.put("designVariables", orderedOutputValueKeys);
        configuration.put("initValues", initValues);
        configuration.put("minValuesVariables", minValues);
        configuration.put("maxValuesVariables", maxValues);
        configuration.put("baseValues", baseValues);
        configuration.put("stepValues", stepValues);
        configuration.put("discreteValues", discreteValues);

    }

    private String getVectorName(String key) {
        return key.substring(0, key.indexOf(OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL));
    }

    @SuppressWarnings("unchecked")
    @Override
    public void readOutputFileFromExternalProgram(Map<String, TypedDatum> outputValueMap) throws IOException {
        if (messageFromClient == null) {
            return;
        }

        String currentWorkingDir = messageFromClient.getCurrentWorkingDir();
        if (currentWorkingDir == null) {
            return;
        }

        File[] cwdFiles = new File(currentWorkingDir).listFiles();
        if (cwdFiles == null) {
            return;
        }

        File outputFile = cwdFiles[0];
        ObjectMapper mapper = JsonUtils.getDefaultObjectMapper();
        Map<String, Object> result = mapper.readValue(outputFile, HashMap.class);
        List<Double> outputs = (List<Double>) result.get("designVar");
        int offset = 0;
        for (int i = 0; i < orderedOutputValueKeys.size(); i += offset) {
            String key = orderedOutputValueKeys.get(i);
            String realKey = "";
            if (key.contains(OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL)) {
                realKey = key.substring(0, key.lastIndexOf(OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL));
            }

            if (!realKey.isEmpty()) {
                if (compContext.getOutputDataType(realKey) == DataType.Vector && !orderedOutputValueKeys.contains(realKey)) {
                    VectorTD resultVec = typedDatumFactory.createVector(Integer.parseInt(compContext.getOutputMetaDataValue(
                        realKey, OptimizerComponentConstants.METADATA_VECTOR_SIZE)));
                    for (int j = 0; j < resultVec.getRowDimension(); j++) {
                        resultVec.setFloatTDForElement(typedDatumFactory.createFloat(outputs.get(i + j)), j);
                    }
                    offset = resultVec.getRowDimension() + 1;
                    outputValueMap.put(realKey, resultVec);
                }
            } else {
                outputValueMap.put(key, typedDatumFactory.createFloat(outputs.get(i)));
                offset = 1;
            }
        }
        gradRequest = (Boolean) result.get("gradRequest");
    }

    @Override
    public boolean getDerivativedNeeded() {
        return gradRequest;
    }

    @Override
    protected void writeInputFileforExternalProgram(Map<String, Double> functionVariables,
        Map<String, Double> functionVariablesGradients, Map<String, Double> constraintVariables, String outputFileName) throws IOException {
        File rceInputFile = new File(messageFromClient.getCurrentWorkingDir(), outputFileName);

        for (String key : functionVariables.keySet()) {
            String name = key;
            if (name.contains(OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL)
                && !name.contains(OptimizerComponentConstants.GRADIENT_DELTA)) {
                name = name.substring(0, name.lastIndexOf(OptimizerComponentConstants.OPTIMIZER_VECTOR_INDEX_SYMBOL));
            }
            if (!name.contains(OptimizerComponentConstants.GRADIENT_DELTA)
                && compContext.getInputMetaDataValue(name, OptimizerComponentConstants.META_GOAL).equals("Maximize")) { // Maximize
                // Optimizer only minimizes functions so for maximizing you need to minimize -f(x)
                functionVariables.put(key, functionVariables.get(key) * NEGATE_VALUE);
            }
        }

        // Arrange gradients
        Map<String, List<Double>> gradientsPerObjectiveOrConstraint = new HashMap<>();

        getGradients(functionVariables, functionVariablesGradients, gradientsPerObjectiveOrConstraint, orderedOutputValueKeys);
        getGradients(constraintVariables, functionVariablesGradients, gradientsPerObjectiveOrConstraint, orderedOutputValueKeys);

        ObjectMapper mapper = JsonUtils.getDefaultObjectMapper();
        Map<String, Object> all = new HashMap<>();

        all.put("objective", functionVariables);
        all.put("const", constraintVariables);

        all.put("grad", gradientsPerObjectiveOrConstraint);

        mapper.writerWithDefaultPrettyPrinter().writeValue(rceInputFile, all);
    }

    private void getGradients(Map<String, Double> functionVariables, Map<String, Double> functionVariablesGradients,
        Map<String, List<Double>> gradientsPerObjectiveOrConstraint, List<String> outputNames) {
        for (String obj : functionVariables.keySet()) {
            List<Double> values = new LinkedList<>();
            for (String output : outputNames) {
                if (functionVariablesGradients.containsKey(getGradientName(obj, output))) {
                    values.add(functionVariablesGradients.get(getGradientName(obj, output)));
                }
            }
            if (!values.isEmpty()) {
                gradientsPerObjectiveOrConstraint.put(obj, values);
            }
        }
    }

    private String getGradientName(String obj, String output) {
        return OptimizerComponentConstants.GRADIENT_DELTA + obj + DOT + OptimizerComponentConstants.GRADIENT_DELTA + output;
    }

}
