package org.jbehave.core.configuration;

import static java.util.Arrays.asList;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.jbehave.core.steps.StepType;

/**
 * Provides the keywords which allow parsers to find steps in stories and match
 * those steps with candidates through the annotations. It provides the starting
 * words (Given, When, Then And, "!--") using in parsing, as well as providing
 * keywords used in reporting.
 */
@SuppressWarnings("checkstyle:MemberName")
public class Keywords {

    private static final String SYNONYM_SEPARATOR = "\\|";
    
    public static final String META = "Meta";
    public static final String META_PROPERTY = "MetaProperty";
    public static final String NARRATIVE = "Narrative";
    public static final String IN_ORDER_TO = "InOrderTo";
    public static final String AS_A = "AsA";
    public static final String I_WANT_TO = "IWantTo";
    public static final String SO_THAT = "SoThat";
    public static final String SCENARIO = "Scenario";
    public static final String GIVEN_STORIES = "GivenStories";
    public static final String LIFECYCLE = "Lifecycle";
    public static final String BEFORE = "Before";
    public static final String AFTER = "After";
    public static final String EXAMPLES_TABLE = "ExamplesTable";
    public static final String EXAMPLES_TABLE_ROW = "ExamplesTableRow";
    public static final String EXAMPLES_TABLE_HEADER_SEPARATOR = "ExamplesTableHeaderSeparator";
    public static final String EXAMPLES_TABLE_VALUE_SEPARATOR = "ExamplesTableValueSeparator";
    public static final String EXAMPLES_TABLE_IGNORABLE_SEPARATOR = "ExamplesTableIgnorableSeparator";
    public static final String GIVEN = "Given";
    public static final String WHEN = "When";
    public static final String THEN = "Then";
    public static final String AND = "And";
    public static final String IGNORABLE = "Ignorable";
    public static final String COMPOSITE = "Composite";
    public static final String PRIORITY = "Priority";
    public static final String PENDING = "Pending";
    public static final String NOT_PERFORMED = "NotPerformed";
    public static final String FAILED = "Failed";
    public static final String DRY_RUN = "DryRun";
    public static final String STORY_CANCELLED = "StoryCancelled";
    public static final String DURATION = "Duration";
    public static final String SCOPE = "Scope";
    public static final String SCOPE_STEP = "ScopeStep";
    public static final String SCOPE_SCENARIO = "ScopeScenario";
    public static final String SCOPE_STORY = "ScopeStory";
    public static final String OUTCOME = "Outcome";
    public static final String OUTCOME_ANY = "OutcomeAny";
    public static final String OUTCOME_SUCCESS = "OutcomeSuccess";
    public static final String OUTCOME_FAILURE = "OutcomeFailure";
    public static final String OUTCOME_DESCRIPTION = "OutcomeDescription";
    public static final String OUTCOME_VALUE = "OutcomeValue";
    public static final String OUTCOME_MATCHER = "OutcomeMatcher";
    public static final String OUTCOME_VERIFIED = "OutcomeVerified";
    public static final String META_FILTER = "MetaFilter";
    public static final String YES = "Yes";
    public static final String NO = "No";

    public static final List<String> KEYWORDS = asList(
            META,
            META_PROPERTY,
            NARRATIVE,
            IN_ORDER_TO,
            AS_A,
            I_WANT_TO,
            SO_THAT,
            SCENARIO,
            GIVEN_STORIES,
            LIFECYCLE,
            BEFORE,
            AFTER,
            EXAMPLES_TABLE,
            EXAMPLES_TABLE_ROW,
            EXAMPLES_TABLE_HEADER_SEPARATOR,
            EXAMPLES_TABLE_VALUE_SEPARATOR,
            EXAMPLES_TABLE_IGNORABLE_SEPARATOR,
            GIVEN,
            WHEN,
            THEN,
            AND,
            IGNORABLE,
            COMPOSITE,
            PRIORITY,
            PENDING,
            NOT_PERFORMED,
            FAILED,
            DRY_RUN,
            STORY_CANCELLED,
            DURATION,
            SCOPE,
            SCOPE_STEP,
            SCOPE_SCENARIO,
            SCOPE_STORY,
            OUTCOME,
            OUTCOME_ANY,
            OUTCOME_SUCCESS,
            OUTCOME_FAILURE,
            OUTCOME_DESCRIPTION,
            OUTCOME_VALUE,
            OUTCOME_MATCHER,
            OUTCOME_VERIFIED,
            META_FILTER,
            YES,
            NO
    );


    private final String meta;
    private final String metaProperty;
    private final String narrative;
    private final String inOrderTo;
    private final String asA;
    private final String iWantTo;
    private final String soThat;
    private final String scenario;
    private final String givenStories;
    private final String lifecycle;
    private final String before;
    private final String after;
    private final String examplesTable;
    private final String examplesTableRow;
    private final String examplesTableHeaderSeparator;
    private final String examplesTableValueSeparator;
    private final String examplesTableIgnorableSeparator;
    private final String given;
    private final String when;
    private final String then;
    private final String and;
    private final String ignorable;
    private final String composite;
    private final String priority;
    private final String pending;
    private final String notPerformed;
    private final String failed;
    private final String dryRun;
    private final String storyCancelled;
    private final String duration;
    private final String scope;
    private final String scopeStep;
    private final String scopeScenario;
    private final String scopeStory;
    private final String outcome;
    private final String outcomeAny;
    private final String outcomeSuccess;
    private final String outcomeFailure;
    private final String outcomeDescription;
    private final String outcomeValue;
    private final String outcomeMatcher;
    private final String outcomeVerified;
    private final String metaFilter;
    private final String yes;
    private final String no;
    private final Map<StepType, String> startingWordsByType = new HashMap<>();


    public static Map<String, String> defaultKeywords() {
        Map<String, String> keywords = new HashMap<>();
        keywords.put(META, "Meta:");
        keywords.put(META_PROPERTY, "@");
        keywords.put(NARRATIVE, "Narrative:");
        keywords.put(IN_ORDER_TO, "In order to");
        keywords.put(AS_A, "As a");
        keywords.put(I_WANT_TO, "I want to");
        keywords.put(SO_THAT, "So that");
        keywords.put(SCENARIO, "Scenario:");
        keywords.put(GIVEN_STORIES, "GivenStories:");
        keywords.put(LIFECYCLE, "Lifecycle:");
        keywords.put(BEFORE, "Before:");
        keywords.put(AFTER, "After:");
        keywords.put(EXAMPLES_TABLE, "Examples:");
        keywords.put(EXAMPLES_TABLE_ROW, "Example:");
        keywords.put(EXAMPLES_TABLE_HEADER_SEPARATOR, "|");
        keywords.put(EXAMPLES_TABLE_VALUE_SEPARATOR, "|");
        keywords.put(EXAMPLES_TABLE_IGNORABLE_SEPARATOR, "|--");
        keywords.put(GIVEN, "Given");
        keywords.put(WHEN, "When");
        keywords.put(THEN, "Then");
        keywords.put(AND, "And");
        keywords.put(IGNORABLE, "!--");
        keywords.put(COMPOSITE, "Composite:");
        keywords.put(PRIORITY, "Priority:");
        keywords.put(PENDING, "PENDING");
        keywords.put(NOT_PERFORMED, "NOT PERFORMED");
        keywords.put(FAILED, "FAILED");
        keywords.put(DRY_RUN, "DRY RUN");
        keywords.put(STORY_CANCELLED, "STORY CANCELLED");
        keywords.put(DURATION, "DURATION");
        keywords.put(SCOPE, "Scope:");
        keywords.put(SCOPE_STEP, "STEP");
        keywords.put(SCOPE_SCENARIO, "SCENARIO");
        keywords.put(SCOPE_STORY, "STORY");
        keywords.put(OUTCOME, "Outcome:");
        keywords.put(OUTCOME_ANY, "ANY");
        keywords.put(OUTCOME_SUCCESS, "SUCCESS");
        keywords.put(OUTCOME_FAILURE, "FAILURE");
        keywords.put(OUTCOME_DESCRIPTION, "DESCRIPTION");
        keywords.put(OUTCOME_MATCHER, "MATCHER");
        keywords.put(OUTCOME_VALUE, "VALUE");
        keywords.put(OUTCOME_VERIFIED, "VERIFIED");
        keywords.put(META_FILTER, "MetaFilter:");
        keywords.put(YES, "Yes");
        keywords.put(NO, "No");
        return keywords;
    }

    /**
     * Creates Keywords with default values {@link #defaultKeywords()}
     */
    public Keywords() {
        this(defaultKeywords());
    }

    /**
     * Creates Keywords with provided keywords Map and Encoding
     * 
     * @param keywords the Map of keywords indexed by their name
     */
    public Keywords(Map<String, String> keywords) {
        this.meta = keyword(META, keywords);
        this.metaProperty = keyword(META_PROPERTY, keywords);
        this.narrative = keyword(NARRATIVE, keywords);
        this.inOrderTo = keyword(IN_ORDER_TO, keywords);
        this.asA = keyword(AS_A, keywords);
        this.iWantTo = keyword(I_WANT_TO, keywords);
        this.soThat = keyword(SO_THAT, keywords);
        this.scenario = keyword(SCENARIO, keywords);
        this.givenStories = keyword(GIVEN_STORIES, keywords);
        this.lifecycle = keyword(LIFECYCLE, keywords);
        this.before = keyword(BEFORE, keywords);
        this.after = keyword(AFTER, keywords);
        this.examplesTable = keyword(EXAMPLES_TABLE, keywords);
        this.examplesTableRow = keyword(EXAMPLES_TABLE_ROW, keywords);
        this.examplesTableHeaderSeparator = keyword(EXAMPLES_TABLE_HEADER_SEPARATOR, keywords);
        this.examplesTableValueSeparator = keyword(EXAMPLES_TABLE_VALUE_SEPARATOR, keywords);
        this.examplesTableIgnorableSeparator = keyword(EXAMPLES_TABLE_IGNORABLE_SEPARATOR, keywords);
        this.given = keyword(GIVEN, keywords);
        this.when = keyword(WHEN, keywords);
        this.then = keyword(THEN, keywords);
        this.and = keyword(AND, keywords);
        this.ignorable = keyword(IGNORABLE, keywords);
        this.composite = keyword(COMPOSITE, keywords);
        this.priority = keyword(PRIORITY, keywords);
        this.pending = keyword(PENDING, keywords);
        this.notPerformed = keyword(NOT_PERFORMED, keywords);
        this.failed = keyword(FAILED, keywords);
        this.dryRun = keyword(DRY_RUN, keywords);
        this.storyCancelled = keyword(STORY_CANCELLED, keywords);
        this.duration = keyword(DURATION, keywords);
        this.scope = keyword(SCOPE, keywords);
        this.scopeStep = keyword(SCOPE_STEP, keywords);
        this.scopeScenario = keyword(SCOPE_SCENARIO, keywords);
        this.scopeStory = keyword(SCOPE_STORY, keywords);
        this.outcome = keyword(OUTCOME, keywords);
        this.outcomeAny = keyword(OUTCOME_ANY, keywords);
        this.outcomeSuccess = keyword(OUTCOME_SUCCESS, keywords);
        this.outcomeFailure = keyword(OUTCOME_FAILURE, keywords);
        this.outcomeDescription = keyword(OUTCOME_DESCRIPTION, keywords);
        this.outcomeMatcher = keyword(OUTCOME_MATCHER, keywords);
        this.outcomeValue = keyword(OUTCOME_VALUE, keywords);
        this.outcomeVerified = keyword(OUTCOME_VERIFIED, keywords);
        this.metaFilter = keyword(META_FILTER, keywords);
        this.yes = keyword(YES, keywords);
        this.no = keyword(NO, keywords);

        startingWordsByType.put(StepType.GIVEN, given());
        startingWordsByType.put(StepType.WHEN, when());
        startingWordsByType.put(StepType.THEN, then());
        startingWordsByType.put(StepType.AND, and());
        startingWordsByType.put(StepType.IGNORABLE, ignorable());

    }

    private String keyword(String name, Map<String, String> keywords) {
        String keyword = keywords.get(name);
        if (keyword == null) {
            throw new KeywordNotFound(name, keywords);
        }
        return keyword;
    }

    public String meta() {
        return meta;
    }

    public String metaProperty() {
        return metaProperty;
    }

    public String narrative() {
        return narrative;
    }

    public String inOrderTo() {
        return inOrderTo;
    }

    public String asA() {
        return asA;
    }

    @SuppressWarnings("checkstyle:MethodName")
    public String iWantTo() {
        return iWantTo;
    }

    public String soThat() {
        return soThat;
    }

    public String scenario() {
        return scenario;
    }

    public String givenStories() {
        return givenStories;
    }

    public String lifecycle() {
        return lifecycle;
    }

    public String before() {
        return before;
    }

    public String after() {
        return after;
    }

    public String examplesTable() {
        return examplesTable;
    }

    public String examplesTableRow() {
        return examplesTableRow;
    }

    public String examplesTableHeaderSeparator() {
        return examplesTableHeaderSeparator;
    }

    public String examplesTableValueSeparator() {
        return examplesTableValueSeparator;
    }

    public String examplesTableIgnorableSeparator() {
        return examplesTableIgnorableSeparator;
    }

    public String given() {
        return given;
    }

    public String when() {
        return when;
    }

    public String then() {
        return then;
    }

    public String and() {
        return and;
    }

    public String ignorable() {
        return ignorable;
    }

    public String composite() {
        return composite;
    }

    public String priority() {
        return priority;
    }

    public String pending() {
        return pending;
    }

    public String notPerformed() {
        return notPerformed;
    }

    public String failed() {
        return failed;
    }

    public String dryRun() {
        return dryRun;
    }

    public String storyCancelled() {
        return storyCancelled;
    }

    public String duration() {
        return duration;
    }

    public String scope() {
        return scope;
    }

    public String scopeStep() {
        return scopeStep;
    }

    public String scopeScenario() {
        return scopeScenario;
    }

    public String scopeStory() {
        return scopeStory;
    }

    public String outcome() {
        return outcome;
    }

    public String outcomeAny() {
        return outcomeAny;
    }

    public String outcomeSuccess() {
        return outcomeSuccess;
    }

    public String outcomeFailure() {
        return outcomeFailure;
    }

    public String outcomeDescription() {
        return outcomeDescription;
    }

    public String outcomeValue() {
        return outcomeValue;
    }

    public String outcomeMatcher() {
        return outcomeMatcher;
    }

    public String outcomeVerified() {
        return outcomeVerified;
    }

    public List<String> outcomeFields() {
        return asList(outcomeDescription, outcomeValue, outcomeMatcher, outcomeVerified);
    }

    public String metaFilter() {
        return metaFilter;
    }

    public String yes() {
        return yes;
    }

    public String no() {
        return no;
    }

    public String[] synonymsOf(String word) {
        return word.split(SYNONYM_SEPARATOR);
    }

    public Stream<String> startingWords(Predicate<StepType> stepTypeFilter) {
        return startingWordsByType()
                .entrySet()
                .stream()
                .filter(e -> stepTypeFilter.test(e.getKey()))
                .map(Entry::getValue)
                .map(this::synonymsOf)
                .flatMap(Stream::of);
    }

    public Map<StepType, String> startingWordsByType() {
        return startingWordsByType;
    }

    private boolean ofStepType(String stepAsString, StepType stepType) {
        boolean isType = false;
        for (String word : startingWordsFor(stepType)) {
            isType = stepStartsWithWord(stepAsString, word);
            if (isType) {
                break;
            }
        }
        return isType;
    }

    public boolean isAndStep(String stepAsString) {
        return ofStepType(stepAsString, StepType.AND);
    }

    public boolean isIgnorableStep(String stepAsString) {
        return ofStepType(stepAsString, StepType.IGNORABLE);
    }

    public String stepWithoutStartingWord(String stepAsString, StepType stepType) {
        String startingWord = startingWord(stepAsString, stepType);
        return stepAsString.substring(startingWord.length() + 1); // 1 for the space after
    }

    public String stepWithoutStartingWord(String stepAsString) {
        StepType stepType = stepTypeFor(stepAsString);
        return stepWithoutStartingWord(stepAsString, stepType);
    }

    public String startingWord(String stepAsString, StepType stepType) throws StartingWordNotFound {
        for (String wordForType : startingWordsFor(stepType)) {
            if (stepStartsWithWord(stepAsString, wordForType)) {
                return wordForType;
            }
        }
        for (String andWord : startingWordsFor(StepType.AND)) {
            if (stepStartsWithWord(stepAsString, andWord)) {
                return andWord;
            }
        }
        throw new StartingWordNotFound(stepAsString, stepType, startingWordsByType);
    }

    public String startingWord(String stepAsString) throws StartingWordNotFound {
        for (StepType stepType : startingWordsByType.keySet()) {
            for (String wordForType : startingWordsFor(stepType)) {
                if (stepStartsWithWord(stepAsString, wordForType)) {
                    return wordForType;
                }
            }
        }
        throw new StartingWordNotFound(stepAsString, startingWordsByType);
    }

    public StepType stepTypeFor(String stepAsString) throws StartingWordNotFound {
        for (StepType stepType : startingWordsByType.keySet()) {
            for (String wordForType : startingWordsFor(stepType)) {
                if (stepStartsWithWord(stepAsString, wordForType)) {
                    return stepType;
                }
            }
        }
        throw new StartingWordNotFound(stepAsString, startingWordsByType);
    }

    public boolean stepStartsWithWord(String step, String word) {
        return stepStartsWithWords(step, word);
    }

    public boolean stepStartsWithWords(String step, String... words) {
        char separator = ' '; // space after qualifies it as word
        String start = StringUtils.join(words, separator) + separator;
        return step.startsWith(start);
    }

    public String startingWordFor(StepType stepType) {
        String startingWord = startingWordsByType.get(stepType);
        if (startingWord == null) {
            throw new StartingWordNotFound(stepType, startingWordsByType);
        }
        return startingWord;
    }

    public String[] startingWordsFor(StepType stepType) {
        return synonymsOf(startingWordFor(stepType));
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }

    @SuppressWarnings("serial")
    public static class KeywordNotFound extends RuntimeException {

        public KeywordNotFound(String name, Map<String, String> keywords) {
            super("Keyword " + name + " not found amongst " + keywords);
        }

    }

    @SuppressWarnings("serial")
    public static class StartingWordNotFound extends RuntimeException {

        public StartingWordNotFound(String step, StepType stepType, Map<StepType, String> startingWordsByType) {
            super("No starting word found for step '" + step + "' of type '" + stepType + "' amongst '"
                    + startingWordsByType + "'");
        }

        public StartingWordNotFound(String step, Map<StepType, String> startingWordsByType) {
            super("No starting word found for step '" + step + "' amongst '" + startingWordsByType + "'");
        }

        public StartingWordNotFound(StepType stepType, Map<StepType, String> startingWordsByType) {
            super("No starting word found of type '" + stepType + "' amongst '" + startingWordsByType + "'");
        }
    }
}
