/*******************************************************************************
 * (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.parser.utils;

import io.cloudslang.lang.compiler.SlangSource;
import io.cloudslang.lang.compiler.parser.MetadataParser;
import io.cloudslang.lang.compiler.parser.model.ParsedDescriptionData;
import io.cloudslang.lang.compiler.parser.model.ParsedDescriptionSection;
import io.cloudslang.lang.compiler.utils.MetadataUtils;
import io.cloudslang.lang.compiler.utils.SlangSourceUtils;
import io.cloudslang.lang.compiler.validator.matcher.DescriptionPatternMatcher;
import io.cloudslang.lang.entities.constants.Regex;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.Pair;

public class MetadataValidatorImpl implements MetadataValidator {

    private MetadataParser metadataParser;
    private DescriptionPatternMatcher descriptionPatternMatcher;

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

    @Override
    public List<RuntimeException> validateCheckstyle(SlangSource source) {
        Validate.notNull(source.getContent(), "Source " + source.getName() + " cannot be null");
        try {
            return extractCheckstyleData(source);
        } catch (Throwable e) {
            throw new RuntimeException(
                    "There was a problem extracting checkstyle data for source [" +
                            source.getName() + "]  - " + e.getMessage(), e
            );
        }
    }

    private List<RuntimeException> extractCheckstyleData(SlangSource source) {
        List<String> lines = SlangSourceUtils.readLines(source);
        ParsedDescriptionData parsedDescriptionData = metadataParser.parse(source);
        List<RuntimeException> errors = new ArrayList<>();

        // process flow descriptions
        List<ParsedDescriptionSection> topLevelDescriptions = parsedDescriptionData.getTopLevelDescriptions();
        for (ParsedDescriptionSection topLevelDescription : topLevelDescriptions) {
            errors.addAll(processCommonRules(lines, topLevelDescription, false));
        }

        // process step descriptions
        Collection<ParsedDescriptionSection> stepDescriptions = parsedDescriptionData.getStepDescriptions().values();
        for (ParsedDescriptionSection stepDescription : stepDescriptions) {
            errors.addAll(processCommonRules(lines, stepDescription, true));
        }

        return errors;
    }

    private List<RuntimeException> processCommonRules(
            List<String> lines,
            ParsedDescriptionSection parsedDescriptionSection,
            boolean isStep) {
        List<RuntimeException> errors = new ArrayList<>();
        int startLineNumberZeroBased = parsedDescriptionSection.getStartLineNumber() - 1;

        // validate begin wrapper line
        validateBeginWrapperLine(lines, isStep, errors, startLineNumberZeroBased);

        boolean finished = false;
        String previousTag = null;
        int previousItemEndLineNumber = -1;
        for (int lineNrZeroBased = startLineNumberZeroBased + 1;
             !finished && lineNrZeroBased < lines.size();
             lineNrZeroBased++) {
            String currentLine = lines.get(lineNrZeroBased);

            // #! @tag var: content
            // #! @tag: content
            boolean variableLine = isVariableLine(currentLine);
            boolean isGeneralLine = isGeneralLine(currentLine);
            if (variableLine || isGeneralLine || isVariableLineDeclarationOnly(currentLine)) {
                // extract tag
                String currentTag;
                if (variableLine) {
                    Pair<String, String> declaration =
                            descriptionPatternMatcher.getDescriptionVariableLineData(currentLine);
                    String[] declarationElements = descriptionPatternMatcher.splitDeclaration(declaration.getLeft());
                    currentTag = declarationElements[0];
                } else if (isGeneralLine) {
                    Pair<String, String> declaration =
                            descriptionPatternMatcher.getDescriptionGeneralLineData(currentLine);
                    String[] declarationElements = descriptionPatternMatcher.splitDeclaration(declaration.getLeft());
                    currentTag = declarationElements[0];
                } else {
                    Pair<String, String> declaration =
                            descriptionPatternMatcher.getDescriptionVariableLineDataDeclarationOnly(currentLine);
                    String[] declarationElements = descriptionPatternMatcher.splitDeclaration(declaration.getLeft());
                    currentTag = declarationElements[0];
                }

                // validate empty line
                validateEmptyLine(lines, errors, previousTag, previousItemEndLineNumber, currentTag);

                previousTag = currentTag;
                previousItemEndLineNumber = lineNrZeroBased;
            } else {
                // #! continued from previous line
                if (isNonEmptyComplementaryLine(currentLine)) {
                    previousItemEndLineNumber = lineNrZeroBased;
                } else {
                    // #!!#
                    if (descriptionPatternMatcher.matchesDescriptionEnd(currentLine)) {
                        // validate ending wrapper line
                        validateEndingWrapperLine(lines, isStep, errors, lineNrZeroBased);

                        finished = true;
                    }
                    // otherwise ignore
                }
            }
        }
        return errors;
    }

    private void validateEndingWrapperLine(
            List<String> lines,
            boolean isStep,
            List<RuntimeException> errors,
            int lineNrZeroBased) {
        int targetedLineNumberZeroBased = lineNrZeroBased + 1;
        String nextLine = tryExtractLine(lines, targetedLineNumberZeroBased);
        if (isStep) {
            if (nextLine == null || !descriptionPatternMatcher.matchesStepDelimiterLine(nextLine)) {
                errors.add(
                        new RuntimeException(
                                generateErrorMessage(lineNrZeroBased,
                                        "Next line should be delimiter line (90 characters of `#`)"
                                )
                        )
                );
            }
        } else {
            if (nextLine == null || !descriptionPatternMatcher.matchesExecutableDelimiterLine(nextLine)) {
                errors.add(
                        new RuntimeException(
                                generateErrorMessage(lineNrZeroBased,
                                        "Next line should be delimiter line (120 characters of `#`)"
                                )
                        )
                );
            }
        }
    }

    private void validateEmptyLine(
            List<String> lines,
            List<RuntimeException> errors,
            String previousTag,
            int previousItemEndLineNumber,
            String currentTag) {
        if (previousTag != null && !previousTag.equals(currentTag)) {
            int targetedLineNumberZeroBased = previousItemEndLineNumber + 1;
            String targetedLine = lines.get(targetedLineNumberZeroBased);
            if (!descriptionPatternMatcher.matchesEmptyLine(targetedLine)) {
                errors.add(
                        new RuntimeException(
                                generateErrorMessage(
                                        targetedLineNumberZeroBased,
                                        "There should be an empty line between two sections of different tags" +
                                                " (" + previousTag + " and " + currentTag + ")"
                                )
                        )
                );
            }
        }
    }

    private void validateBeginWrapperLine(
            List<String> lines,
            boolean isStep,
            List<RuntimeException> errors,
            int startLineNumberZeroBased) {
        int targetedLineNumberZeroBased = startLineNumberZeroBased - 1;
        String previousLine = tryExtractLine(lines, targetedLineNumberZeroBased);
        if (isStep) {
            if (previousLine == null || !descriptionPatternMatcher.matchesStepDelimiterLine(previousLine)) {
                errors.add(
                        new RuntimeException(
                                generateErrorMessage(startLineNumberZeroBased,
                                        "Previous line should be delimiter line (90 characters of `#`)"
                                )
                        )
                );
            }
        } else {
            if (previousLine == null || !descriptionPatternMatcher.matchesExecutableDelimiterLine(previousLine)) {
                errors.add(
                        new RuntimeException(
                                generateErrorMessage(startLineNumberZeroBased,
                                        "Previous line should be delimiter line (120 characters of `#`)"
                                )
                        )
                );
            }
        }
    }

    private String generateErrorMessage(int lineNumberZeroBased, String data) {
        return MetadataUtils.generateErrorMessage(lineNumberZeroBased, data);
    }

    private boolean isNonEmptyComplementaryLine(String currentLine) {
        return
                descriptionPatternMatcher.matchesDescriptionComplementaryLine(currentLine) &&
                        !currentLine.trim().equals(Regex.DESCRIPTION_TOKEN);
    }

    private boolean isGeneralLine(String currentLine) {
        return descriptionPatternMatcher.matchesDescriptionGeneralLine(currentLine);
    }

    private boolean isVariableLine(String currentLine) {
        return descriptionPatternMatcher.matchesDescriptionVariableLine(currentLine);
    }

    private boolean isVariableLineDeclarationOnly(String currentLine) {
        return descriptionPatternMatcher.matchesVariableLineDeclarationOnlyLine(currentLine);
    }

    private String tryExtractLine(List<String> lines, int lineNr) {
        if (lineNr >= 0 && lineNr < lines.size()) {
            return lines.get(lineNr);
        }
        return null;
    }

    public void setMetadataParser(MetadataParser metadataParser) {
        this.metadataParser = metadataParser;
    }

}
