/*******************************************************************************
 * (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License v2.0 which accompany this distribution.
 *
 * The Apache License is available at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 *******************************************************************************/
package io.cloudslang.lang.compiler.modeller;

import io.cloudslang.lang.compiler.modeller.model.Metadata;
import io.cloudslang.lang.compiler.modeller.model.StepMetadata;
import io.cloudslang.lang.compiler.modeller.result.MetadataModellingResult;
import io.cloudslang.lang.compiler.parser.model.ParsedDescriptionData;
import io.cloudslang.lang.compiler.parser.model.ParsedDescriptionSection;
import io.cloudslang.lang.compiler.parser.utils.DescriptionTag;
import io.cloudslang.lang.compiler.parser.utils.StepDescriptionTag;
import io.cloudslang.lang.compiler.validator.matcher.DescriptionPatternMatcher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

public class MetadataModellerImpl implements MetadataModeller {
    private DescriptionPatternMatcher descriptionPatternMatcher;

    public MetadataModellerImpl() {
        descriptionPatternMatcher = new DescriptionPatternMatcher();
    }

    @Override
    public MetadataModellingResult createModel(ParsedDescriptionData parsedDescriptionData) {
        List<ParsedDescriptionSection> topLevelDescriptions = parsedDescriptionData.getTopLevelDescriptions();
        List<RuntimeException> errors = new ArrayList<>(parsedDescriptionData.getErrors());
        Pair<Metadata, List<RuntimeException>> executableMetadata = null;

        // executable metadata
        if (CollectionUtils.isNotEmpty(topLevelDescriptions)) {
            if (topLevelDescriptions.size() > 1) {
                errors.add(
                        new RuntimeException(
                                "Multiple top level descriptions found at line numbers: " +
                                        getLineNumbers(topLevelDescriptions)
                        )
                );
            }
            executableMetadata =
                    transformToExecutableMetadata(topLevelDescriptions.get(0).getData());
            errors.addAll(executableMetadata.getRight());
        }

        // step metadata
        Pair<List<StepMetadata>, List<RuntimeException>> stepsModellingResult =
                transformStepsData(parsedDescriptionData.getStepDescriptions());
        List<StepMetadata> stepDescriptions = stepsModellingResult.getLeft();
        errors.addAll(stepsModellingResult.getRight());

        return new MetadataModellingResult(
                executableMetadata == null ? new Metadata() : executableMetadata.getLeft(),
                stepDescriptions,
                errors
        );
    }

    private String getLineNumbers(List<ParsedDescriptionSection> topLevelDescriptions) {
        int[] lineNumbers = new int[topLevelDescriptions.size()];
        for (int i = 0; i < topLevelDescriptions.size(); i++) {
            lineNumbers[i] = topLevelDescriptions.get(i).getStartLineNumber();
        }
        return Arrays.toString(lineNumbers);
    }

    private Pair<Metadata, List<RuntimeException>> transformToExecutableMetadata(
            Map<String, String> parsedData) {
        String description = "";
        String prerequisites = "";
        Map<String, String> inputs = new LinkedHashMap<>();
        Map<String, String> outputs = new LinkedHashMap<>();
        Map<String, String> results = new LinkedHashMap<>();
        Map<String, String> systemProperties = new LinkedHashMap<>();
        List<RuntimeException> errors = new ArrayList<>();

        for (Map.Entry<String, String> entry : parsedData.entrySet()) {
            String declaration = entry.getKey();
            String[] declarationElements = descriptionPatternMatcher.splitDeclaration(declaration);
            String tag = declarationElements[0];
            if (DescriptionTag.isDescriptionTag(tag)) {
                String content = entry.getValue();
                DescriptionTag descriptionTag = DescriptionTag.fromString(tag);
                if (descriptionTag != null) {
                    switch (descriptionTag) {
                        case DESCRIPTION:
                            description = content;
                            break;
                        case PREREQUISITES:
                            prerequisites = content;
                            break;
                        case INPUT:
                            processExecutableDeclaration(declarationElements, errors, tag, inputs, content);
                            break;
                        case OUTPUT:
                            processExecutableDeclaration(declarationElements, errors, tag, outputs, content);
                            break;
                        case RESULT:
                            processExecutableDeclaration(declarationElements, errors, tag, results, content);
                            break;
                        case SYSTEM_PROPERTY:
                            processExecutableDeclaration(declarationElements, errors, tag, systemProperties, content);
                            break;
                        default:
                            // shouldn't get here
                            errors.add(new NotImplementedException("Unrecognized tag: " + descriptionTag));
                    }
                } else {
                    // shouldn't get here
                    errors.add(new RuntimeException("Unrecognized tag: " + tag));
                }
            } else {
                errors.add(new RuntimeException("Unrecognized tag for executable description section: " + tag));
            }
        }

        Metadata executableMetadata = new Metadata(
                description,
                prerequisites,
                inputs,
                outputs,
                results,
                systemProperties
        );
        return new ImmutablePair<>(executableMetadata, errors);
    }

    private void processExecutableDeclaration(
            String[] declarationElements,
            List<RuntimeException> errors,
            String tag,
            Map<String, String> parameters,
            String content) {
        processDeclaration(declarationElements, errors, tag, parameters, content, "executable");
    }

    private void processStepDeclaration(
            String[] declarationElements,
            List<RuntimeException> errors,
            String tag,
            Map<String, String> parameters,
            String content,
            String stepName) {
        processDeclaration(declarationElements, errors, tag, parameters, content, "step[" + stepName + "]");
    }

    private void processDeclaration(
            String[] declarationElements,
            List<RuntimeException> errors,
            String tag,
            Map<String, String> parameters,
            String content,
            String identifier) {
        if (declarationElements.length != 2) {
            errors.add(
                    new RuntimeException(
                            "For " + identifier + " parameter name for tag[" + tag + "] is missing. " +
                                    "Format should be [" + tag + " name]"
                    )
            );
        } else {
            String name = declarationElements[1];
            parameters.put(name, content);
        }
    }

    private Pair<List<StepMetadata>, List<RuntimeException>> transformStepsData(
            Map<String, ParsedDescriptionSection> stepsData) {
        List<StepMetadata> stepsMetadata = new ArrayList<>();
        List<RuntimeException> errors = new ArrayList<>();
        for (Map.Entry<String, ParsedDescriptionSection> entry : stepsData.entrySet()) {
            String stepName = entry.getKey();
            ParsedDescriptionSection parsedData = entry.getValue();
            Pair<StepMetadata, List<RuntimeException>> transformResult =
                    transformToStepMetadata(stepName, parsedData.getData());
            stepsMetadata.add(transformResult.getLeft());
            errors.addAll(transformResult.getRight());
        }

        return new ImmutablePair<>(stepsMetadata, errors);
    }

    private Pair<StepMetadata, List<RuntimeException>> transformToStepMetadata(
            String stepName,
            Map<String, String> parsedData) {
        Map<String, String> inputs = new LinkedHashMap<>();
        Map<String, String> outputs = new LinkedHashMap<>();
        List<RuntimeException> errors = new ArrayList<>();

        for (Map.Entry<String, String> entry : parsedData.entrySet()) {
            String declaration = entry.getKey();
            String[] declarationElements = descriptionPatternMatcher.splitDeclaration(declaration);
            String tag = declarationElements[0];
            if (StepDescriptionTag.isStepDescriptionTag(tag)) {
                String content = entry.getValue();
                StepDescriptionTag descriptionTag = StepDescriptionTag.fromString(tag);
                if (descriptionTag != null) {
                    switch (descriptionTag) {
                        case INPUT:
                            processStepDeclaration(declarationElements, errors, tag, inputs, content, stepName);
                            break;
                        case OUTPUT:
                            processStepDeclaration(declarationElements, errors, tag, outputs, content, stepName);
                            break;
                        default:
                            // shouldn't get here
                            errors.add(new NotImplementedException("Unrecognized tag: " + descriptionTag));
                    }
                } else {
                    // shouldn't get here
                    errors.add(new RuntimeException("Unrecognized tag: " + tag));
                }
            } else {
                errors.add(new RuntimeException("Unrecognized tag for step description section: " + tag));
            }
        }

        StepMetadata stepMetadata = new StepMetadata(
                stepName,
                inputs,
                outputs
        );
        return new ImmutablePair<>(stepMetadata, errors);
    }

}
