/*
 * PDDLAction.java
 *
 * Copyright (C) 2020 Vladislav Nikolov Vasilev
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see https://www.gnu.org/licenses/gpl-3.0.html.
 */

/**
 * Package that contains the planning agent along with its data structures.
 */
package controller;

import ontology.Types;
import tools.Pair;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Class that represents a PDDL action. A PDDLAction object contains a PDDL
 * action instance and its corresponding GVGAI action, a list of instantiated
 * preconditions and a list of instantiated effects.
 *
 * @author Vladislav Nikolov Vasilev
 */
public class PDDLAction {
    private String actionInstance;
    private Types.ACTIONS GVGAIAction;
    private List<String> preconditions;
    private List<PDDLEffect> effects;

    /**
     * Class that represents an effect of a PDDL action. An effect is consists of
     * an instantiated effect predicate and a list of conditions that must be met
     * for the effect to take place.
     */
    public class PDDLEffect {
        private String effectPredicate;
        private List<String> conditions;

        /**
         * Class constructor. Creates a new instance with a given effect predicate
         * and a list of conditions.
         *
         * @param effectPredicate Instantiated effect predicate.
         * @param conditions      List of conditions that must be met for the effect to
         *                        take place.
         */
        public PDDLEffect(String effectPredicate, List<String> conditions) {
            this.effectPredicate = effectPredicate;
            this.conditions = conditions;
        }

        /**
         * Class constructor. Creates a new instance from a given effect predicate.
         * The list of conditions will be empty, since there are none.
         *
         * @param effectPredicate Instantiated effect predicate.
         */
        public PDDLEffect(String effectPredicate) {
            this(effectPredicate, new ArrayList<>());
        }

        public String getEffectPredicate() {
            return this.effectPredicate;
        }

        public List<String> getConditions() {
            return this.conditions;
        }

        @Override
        public String toString() {
            return "{ PDDLEffect predicate: " + this.effectPredicate + ", Conditions: " + this.conditions + " }";
        }
    }

    /**
     * Class constructor.
     *
     * @param actionInstance       String that contains an instantiated PDDL action.
     * @param actionDescription    String that contains the description of an action
     *                             (action name, parameters, preconditions and effects).
     * @param actionCorrespondence Map that contains the correspondence from a PDDL
     *                             action to a GVGAI action.
     */
    public PDDLAction(String actionInstance, String actionDescription, Map<String, Types.ACTIONS> actionCorrespondence) {
        this.actionInstance = actionInstance;
        this.GVGAIAction = this.translateActionInstanceToGVGAI(actionCorrespondence);
        this.preconditions = this.processPreconditionsFromActionDescription(actionDescription);
        this.effects = this.processEffectsFromActionDescription(actionDescription);
    }

    public String getActionInstance() {
        return this.actionInstance;
    }

    public List<String> getPreconditions() {
        return this.preconditions;
    }

    public Types.ACTIONS getGVGAIAction() {
        return this.GVGAIAction;
    }

    public List<PDDLEffect> getEffects() {
        return this.effects;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();

        builder.append("\n\n\t### Action ###");
        builder.append(String.format("\n\t|--- Instance: %s", this.actionInstance));
        builder.append(String.format("\n\t|--- GVGAI action: %s", this.GVGAIAction));
        builder.append(String.format("\n\t|--- List of preconditions: %s", this.preconditions));
        builder.append(String.format("\n\t|--- List of effects: %s", this.effects));

        return builder.toString();
    }

    /**
     * Method that transforms the PDDL action instance into a GVGAI action.
     * It used a Map that is passed as parameter in which each PDDL action
     * is associated to a GVGAI action.
     *
     * @param actionCorrespondence Map that contains the correspondence from a PDDL
     *                             action to a GVGAI action.
     * @return Returns the corresponding GVGAI action.
     */
    private Types.ACTIONS translateActionInstanceToGVGAI(Map<String, Types.ACTIONS> actionCorrespondence) {
        // Create pattern
        Pattern actionPattern = Pattern.compile("[^( ]+");

        // Find a match of the pattern
        Matcher actionMatcher = actionPattern.matcher(this.actionInstance);
        actionMatcher.find();

        // Get the action (the first element) and transform it to upper case
        String action = actionMatcher.group(0).toUpperCase();

        return actionCorrespondence.get(action);
    }

    /**
     * Method that reads an action's description and obtains all the preconditions
     * associated to that action. All of the preconditions are instantiated.
     *
     * @param actionDescription String that contains the description of an action
     *                          (action name, parameters, preconditions and effects).
     * @return Returns a list containing all the instantiated preconditions.
     */
    private List<String> processPreconditionsFromActionDescription(String actionDescription) {
        // Get preconditions match
        String preconditionsMatch = this.matchFormatPattern(Pattern.compile(":precondition[^:]+"),
                actionDescription,
                ":precondition");

        // Remove spaces between predicates
        preconditionsMatch = preconditionsMatch.replaceAll("\\) \\(", "\\)\\(");

        return Parse(preconditionsMatch);
    }

    // =======================================================================================================
    /**
     * Method that parse a set of predicates while grouping them (support for "and" and "or")
     *
     * @param theString String with all the predicates.
     * @return Returns a list containing all the instantiated preconditions.
     */
    // =======================================================================================================
    private ArrayList<String> Parse(String theString) {
        ArrayList<String> theStrings = new ArrayList<String>();
        StringBuffer temp = new StringBuffer("");
        int numOpenParentheses = 0;

        for (int i = 0; i < theString.length() ; i++) {
            char cTemp = theString.charAt(i);
            temp.append(cTemp);
            switch (cTemp) {
                case '(': {
                    numOpenParentheses++;
                    break;
                }
                case ')': {
                    numOpenParentheses--;
                    if (numOpenParentheses == 0) {
                        theStrings.add(temp.toString());
                        temp.delete(0,temp.length());
                    }
                }
                default: { }
            }
        }
        return theStrings;
    }
    // =======================================================================================================
    // =======================================================================================================

    /**
     * Method that reads an action's description and obtains all the effects
     * associated to that action. All of the effects are instantiated.
     *
     * @param actionDescription String that contains the description of an action
     *                          (action name, parameters, preconditions and effects).
     * @return Returns a list containing the instantiated effects.
     */
    private List<PDDLEffect> processEffectsFromActionDescription(String actionDescription) {
        // Get effects match
        String effectsMatch = this.matchFormatPattern(Pattern.compile(":effect[^:]+"),
                actionDescription,
                ":effect");

        // Split effects string into a list of effects
        List<String> splitEffectsList = this.splitDescriptionIntoBlocks(effectsMatch);
        List<PDDLEffect> actionEffects = new ArrayList<>();

        // Process each effect
        for (String effect : splitEffectsList) {
            if (effect.contains("when")) {
                // Split effect into a list of conditions and a list of effects
                Pair<List<String>, List<String>> conditionsEffectsPair = this.splitConditionsFromEffects(effect);
                List<String> conditionsList = conditionsEffectsPair.first;
                List<String> effectsList = conditionsEffectsPair.second;

                effectsList.stream().forEach(e -> actionEffects.add(new PDDLEffect(e, conditionsList)));
            } else {
                actionEffects.add(new PDDLEffect(effect));
            }
        }

        return actionEffects;
    }

    /**
     * Method that matches a part of an action description, removing all extra
     * spaces and parentheses. It can be used to retrieve the effects and the
     * preconditions from an action description.
     *
     * @param pattern       Pattern to be found in the description.
     * @param description   String containing the action's description.
     * @param removeElement Element to be removed from the string.
     * @return Returns a formatted string containing the match.
     */
    private String matchFormatPattern(Pattern pattern, String description, String removeElement) {
        // Get match of the pattern
        Matcher matcher = pattern.matcher(description);
        matcher.find();

        String match = matcher.group(0);

        // Remove all extra spaces, spaces between consequent parentheses and ":effect" at the beginning
        match = match.replaceAll("\\s+", " ");
        match = match.substring(0, match.length() - 1);
        match = match.replaceAll(" \\)", ")");
        match = match.replaceAll(String.format("%s\\s+", removeElement), "");

        // Remove and statement, if there's any. Also removes last parenthesis
        if (match.contains("and")) {
            match = match.replaceFirst("\\(and ", "");
            match = match.substring(0, match.length() - 1);
        }

        // Remove all extra parentheses
        int numOpenParentheses = match.length() - match.replace("(", "").length();
        int numCloseParentheses = match.length() - match.replace(")", "").length();
        int diffParentheses = numOpenParentheses - numCloseParentheses;

        if (numOpenParentheses - numCloseParentheses != 0) {
            match = match.substring(0, match.length() - Math.abs(diffParentheses));
        }

        return match;
    }

    /**
     * Method that splits the conditions from the effects.
     *
     * @param description String which contains the conditions and the effects.
     * @return Returns a pair containing a list of conditions and a list of
     * effects which depend on those conditions.
     */
    private Pair<List<String>, List<String>> splitConditionsFromEffects(String description) {
        // Process description removing (when and last parenthesis
        String processedDescription = description.replace("(when ", "");
        processedDescription = processedDescription.substring(0, processedDescription.length() - 1);

        // Separate effects and conditions in two blocks
        List<String> effectsConditionsBlocks = this.splitDescriptionIntoBlocks(processedDescription);
        String conditions = effectsConditionsBlocks.get(0);
        String effects = effectsConditionsBlocks.get(1);

        // Remove "(and" and last parenthesis, if there's any
        if (conditions.contains("(and")) {
            conditions = conditions.replace("(and", "");
            conditions = conditions.substring(0, conditions.length() - 1);
        }

        if (effects.contains("(and")) {
            effects = effects.replace("(and", "");
            effects = effects.substring(0, effects.length() - 1);
        }

        List<String> conditionsList = this.splitDescriptionIntoBlocks(conditions);
        List<String> effectsList = this.splitDescriptionIntoBlocks(effects);

        Pair<List<String>, List<String>> conditionsEffectsPair = new Pair(conditionsList, effectsList);

        return conditionsEffectsPair;
    }

    /**
     * Method that allows to split a given description into parenthesized blocks
     * according to the opened and closed parentheses found in the string.
     *
     * @param description String to be split into blocks.
     * @return Returns a list of parenthesized blocks.
     */
    private List<String> splitDescriptionIntoBlocks(String description) {
        List<String> descriptionList = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        int numParentheses = 0;

        for (char c : description.toCharArray()) {
            boolean changed = false;

            if (c == '(') {
                numParentheses++;
                changed = true;
            } else if (c == ')') {
                numParentheses--;
                changed = true;
            }

            builder.append(c);

            // Convert builder to string if a final closing parenthesis is found
            if (numParentheses == 0 && changed) {
                descriptionList.add(builder.toString().trim());
                builder.setLength(0);
            }
        }

        return descriptionList;
    }
}
