/*******************************************************************************
 * Copyright (c) 2017 École Polytechnique de Montréal
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.compile;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.Activator;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenAction;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenActionConditional;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenActionList;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenActionSegment;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenActionStateChange;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenActionStateChange.StackAction;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenCondition;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenStateSystemPath;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.values.DataDrivenValue;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.values.DataDrivenValueConstant;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.module.XmlUtils;
import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue;
import org.eclipse.tracecompass.tmf.analysis.xml.core.module.TmfXmlStrings;
import org.eclipse.tracecompass.tmf.analysis.xml.core.module.TmfXmlUtils;
import org.w3c.dom.Element;

/**
 * The compilation unit for an XML state change element
 *
 * FIXME: When porting the XML pattern to these classes, see if state change can
 * be an action
 *
 * @author Geneviève Bastien
 * @author Florian Wininger
 */
public abstract class TmfXmlActionCu implements IDataDrivenCompilationUnit {

    /** A state change assigning a value to a path in the state system */
    private static class TmfXmlStateChangeAssignationCu extends TmfXmlActionCu {

        private TmfXmlStateSystemPathCu fPath;
        private TmfXmlStateValueCu fRightOperand;
        private boolean fIncrement;
        private boolean fUpdate;
        private StackAction fStackAction;
        private @Nullable TmfXmlStateValueCu fFutureTime;

        public TmfXmlStateChangeAssignationCu(TmfXmlStateSystemPathCu path,
                TmfXmlStateValueCu rightOperandCu, boolean increment, boolean update, StackAction stackAction, @Nullable TmfXmlStateValueCu futureTime) {
            fPath = path;
            fRightOperand = rightOperandCu;
            fIncrement = increment;
            fUpdate = update;
            fStackAction = stackAction;
            fFutureTime = futureTime;
        }

        @Override
        public DataDrivenAction generate() {
            DataDrivenStateSystemPath path = fPath.generate();
            DataDrivenValue rightOperand = fRightOperand.generate();
            DataDrivenValue futureTime = null;
            if (fFutureTime != null) {
                futureTime = fFutureTime.generate();
            }
            return new DataDrivenActionStateChange(path, rightOperand, fIncrement, fUpdate, fStackAction, futureTime);
        }

    }

    /** Conditional state change */
    private static class TmfXmlConditionalStateChangeCu extends TmfXmlActionCu {

        private final TmfXmlConditionCu fCondition;
        private final TmfXmlActionCu fThen;
        private final @Nullable TmfXmlActionCu fElse;

        public TmfXmlConditionalStateChangeCu(TmfXmlConditionCu condition, TmfXmlActionCu thenChange, @Nullable TmfXmlActionCu elseChange) {
            fCondition = condition;
            fThen = thenChange;
            fElse = elseChange;
        }

        @Override
        public DataDrivenAction generate() {
            DataDrivenCondition condition = fCondition.generate();
            DataDrivenAction thenChange = fThen.generate();
            DataDrivenAction elseChange = (fElse != null ? fElse.generate() : null);
            return new DataDrivenActionConditional(condition, thenChange, elseChange);
        }

    }

    /** Action list action */
    private static class TmfXmlActionListCu extends TmfXmlActionCu {

        private final List<TmfXmlActionCu> fActions;

        public TmfXmlActionListCu(List<TmfXmlActionCu> actions) {
            fActions = actions;
        }

        @Override
        public DataDrivenAction generate() {
            List<DataDrivenAction> actions = fActions.stream()
                    .map(TmfXmlActionCu::generate)
                    .collect(Collectors.toList());
            return new DataDrivenActionList(actions);
        }

    }

    /** No action */
    private static final TmfXmlActionCu NO_ACTION_CU = new TmfXmlActionCu() {

        @Override
        public DataDrivenAction generate() {
            return DataDrivenAction.NO_ACTION;
        }
    };

    /** Segment creation action */
    private static class TmfXmlActionSegmentCu extends TmfXmlActionCu {

        private final TmfXmlStateValueCu fType;
        private final @Nullable TmfXmlStateValueCu fStart;
        private final @Nullable TmfXmlStateValueCu fDuration;
        private final @Nullable TmfXmlStateValueCu fEnd;
        private final Map<String, TmfXmlStateValueCu> fFieldMap;

        public TmfXmlActionSegmentCu(TmfXmlStateValueCu segmentTypeValue, @Nullable TmfXmlStateValueCu beginTimeValue, @Nullable TmfXmlStateValueCu durationValue, @Nullable TmfXmlStateValueCu endTimeValue, Map<String, TmfXmlStateValueCu> fieldMap) {
            fType = segmentTypeValue;
            fStart = beginTimeValue;
            fDuration = durationValue;
            fEnd = endTimeValue;
            fFieldMap = fieldMap;
        }

        @Override
        public DataDrivenAction generate() {
            DataDrivenValue type = fType.generate();
            DataDrivenValue start = fStart != null ? fStart.generate() : null;
            DataDrivenValue end = fEnd != null ? fEnd.generate() : null;
            DataDrivenValue duration = fDuration != null ? fDuration.generate() : null;
            Map<String, DataDrivenValue> fields = new HashMap<>();
            for (Entry<String, TmfXmlStateValueCu> fieldEntry : fFieldMap.entrySet()) {
                fields.put(fieldEntry.getKey(), fieldEntry.getValue().generate());
            }
            return new DataDrivenActionSegment(type, start, duration, end, fields);
        }

    }

    @Override
    public abstract DataDrivenAction generate();

    /**
     * Compile a state change XML element
     *
     * @param analysisData
     *            The analysis data already compiled
     * @param stateChange
     *            The state change XML element to compile
     * @return The state change compilation unit or <code>null</code> if there was a
     *         compilation error
     */
    public static @Nullable TmfXmlActionCu compile(AnalysisCompilationData analysisData, Element stateChange) {
        /*
         * child nodes is either a list of TmfXmlStateAttributes and TmfXmlStateValues,
         * or an if-then-else series of nodes.
         */
        List<Element> childElements = TmfXmlUtils.getChildElements(stateChange, TmfXmlStrings.IF);
        if (childElements.size() == 1) {
            return compileConditionalChange(analysisData, stateChange, childElements.get(0));
        } else if (childElements.size() > 1) {
            // TODO: Validation message here
            Activator.logError("Conditional State Change: There should be only 1 if node"); //$NON-NLS-1$
            return null;
        }
        return compileAssignationChange(analysisData, stateChange);
    }

    /** Compile a conditional state change */
    private static @Nullable TmfXmlActionCu compileConditionalChange(AnalysisCompilationData analysisData, Element stateChange, Element ifNode) {
        // Compile the child of the IF node
        List<@Nullable Element> childElements = XmlUtils.getChildElements(ifNode);
        if (childElements.size() != 1) {
            // TODO: Validation message here
            Activator.logError("There should be only one element under this condition"); //$NON-NLS-1$
            return null;
        }
        Element subCondition = Objects.requireNonNull(childElements.get(0));
        TmfXmlConditionCu condition = TmfXmlConditionCu.compile(analysisData, subCondition);
        if (condition == null) {
            return null;
        }

        // Compile the then element
        List<Element> thenElements = TmfXmlUtils.getChildElements(stateChange, TmfXmlStrings.THEN);
        if (thenElements.size() != 1) {
            // TODO: Validation message here
            Activator.logError("Conditional State Change: There should be 1 and only 1 then element"); //$NON-NLS-1$
            return null;
        }
        TmfXmlActionCu thenChange = compile(analysisData, thenElements.get(0));
        if (thenChange == null) {
            return null;
        }

        // Compile the else element
        List<Element> elseElements = TmfXmlUtils.getChildElements(stateChange, TmfXmlStrings.ELSE);
        if (elseElements.isEmpty()) {
            return new TmfXmlConditionalStateChangeCu(condition, thenChange, null);
        }
        if (thenElements.size() != 1) {
            // TODO: Validation message here
            Activator.logError("Conditional State Change: There should be at most 1 else element"); //$NON-NLS-1$
            return null;
        }
        TmfXmlActionCu elseChange = compile(analysisData, elseElements.get(0));
        if (elseChange == null) {
            return null;
        }
        return new TmfXmlConditionalStateChangeCu(condition, thenChange, elseChange);
    }

    /** Compile an assignation state change */
    private static @Nullable TmfXmlActionCu compileAssignationChange(AnalysisCompilationData analysisContent, Element stateChange) {
        List<@NonNull Element> leftOperands = TmfXmlUtils.getChildElements(stateChange, TmfXmlStrings.STATE_ATTRIBUTE);
        List<@NonNull Element> rightOperands = TmfXmlUtils.getChildElements(stateChange, TmfXmlStrings.STATE_VALUE);
        List<@NonNull Element> futureTimes = TmfXmlUtils.getChildElements(stateChange, TmfXmlStrings.FUTURE_TIME);
        if (rightOperands.size() != 1) {
            // TODO: Validation message here
            Activator.logError("There should only be one state Value in this state change"); //$NON-NLS-1$
        }
        Element rightOperand = rightOperands.get(0);

        TmfXmlStateSystemPathCu path = TmfXmlStateSystemPathCu.compile(analysisContent, leftOperands);
        if (path == null) {
            return null;
        }
        if (futureTimes.size() > 1) {
            // TODO: Validation message here
            Activator.logError("There should at most one future time for this state change"); //$NON-NLS-1$
        }
        Element futureTime = futureTimes.isEmpty() ? null : futureTimes.get(0);
        TmfXmlStateValueCu futureTimeCu = null;
        if (futureTime != null) {
            futureTimeCu = TmfXmlStateValueCu.compileValue(analysisContent, futureTime);
            if (futureTimeCu == null) {
                return null;
            }
        }

        TmfXmlStateValueCu rightOperandCu = TmfXmlStateValueCu.compileValue(analysisContent, rightOperand);
        if (rightOperandCu == null) {
            return null;
        }

        /* Check if there is an increment for the value */
        boolean increment = Boolean.parseBoolean(rightOperand.getAttribute(TmfXmlStrings.INCREMENT));

        /* Check if this value is an update of the ongoing state */
        boolean update = Boolean.parseBoolean(rightOperand.getAttribute(TmfXmlStrings.UPDATE));

        /*
         * Stack Actions : allow to define a stack with PUSH/POP/PEEK methods
         */
        String stack = rightOperand.getAttribute(TmfXmlStrings.ATTRIBUTE_STACK);
        StackAction stackAction = DataDrivenActionStateChange.StackAction.getTypeFromString(stack);

        // Extra attribute validation
        // Update and increment are not valid for stack actions
        if (update && stackAction != StackAction.NONE) {
            Activator.logError("State change: Update cannot be done with stack action " + stackAction); //$NON-NLS-1$
            return null;
        }
        if (increment && stackAction != StackAction.NONE) {
            Activator.logError("State change: Increment cannot be done with stack action " + stackAction); //$NON-NLS-1$
            return null;
        }

        return new TmfXmlStateChangeAssignationCu(path, rightOperandCu, increment, update, stackAction, futureTimeCu);
    }

    /**
     * Compile a named action element, ie an action that contains an ID
     *
     * TODO: Return the action itself instead of the string when everything has
     * moved to new code path
     *
     * @param analysisData
     *            The analysis data already compiled
     * @param namedEl
     *            the XML element corresponding to the action
     * @return The action, or <code>null</code> if there was compilation
     *         errors
     */
    public static @Nullable TmfXmlActionCu compileNamedAction(AnalysisCompilationData analysisData, Element namedEl) {
        String actionId = namedEl.getAttribute(TmfXmlStrings.ID);
        if (actionId.isEmpty()) {
            // TODO: Validation message here
            Activator.logError("The action should have an ID attribute"); //$NON-NLS-1$
            return null;
        }
        List<@Nullable Element> childElements = XmlUtils.getChildElements(namedEl);
        List<TmfXmlActionCu> actionList = new ArrayList<>();
        for (Element child : childElements) {
            final @NonNull Element nonNullChild = NonNullUtils.checkNotNull(child);
            switch (nonNullChild.getNodeName()) {
            case TmfXmlStrings.STATE_CHANGE:
            {
                TmfXmlActionCu action = compile(analysisData, nonNullChild);
                if (action == null) {
                    return null;
                }
                actionList.add(action);
            }
                break;
            case TmfXmlStrings.FSM_SCHEDULE_ACTION:
                // Do not do anything, this action is not supported yet
                // TODO: Validation message here
                Activator.logWarning("Action " + TmfXmlStrings.FSM_SCHEDULE_ACTION + " is not supported yet, it will have no effect"); //$NON-NLS-1$ //$NON-NLS-2$
                break;
            case TmfXmlStrings.SEGMENT:
            {
                TmfXmlActionCu action = compileSegmentAction(analysisData, nonNullChild);
                if (action == null) {
                    return null;
                }
                actionList.add(action);
            }
                break;
            case TmfXmlStrings.ACTION:
            {
                // Compile the subaction
                TmfXmlActionCu subAction = compileNamedAction(analysisData, nonNullChild);
                if (subAction == null) {
                    return null;
                }
                actionList.add(subAction);
            }
                break;
            default:
                // TODO: Validation message here
                Activator.logError("Invalid action type : " + nonNullChild.getNodeName()); //$NON-NLS-1$
            }
        }
        TmfXmlActionCu actionCu = createActionList(actionList);
        analysisData.addAction(actionId, actionCu);
        return actionCu;
    }

    private static @Nullable TmfXmlActionCu compileSegmentAction(AnalysisCompilationData analysisData, Element node) {
        // Compile the segment type
        List<@NonNull Element> segmentType = TmfXmlUtils.getChildElements(node, TmfXmlStrings.SEGMENT_TYPE);
        if (segmentType.size() != 1) {
            // TODO: Validation message here
            Activator.logWarning("Segment action: There should be one and only one " + TmfXmlStrings.SEGMENT_TYPE + " element."); //$NON-NLS-1$ //$NON-NLS-2$
            return null;
        }
        Element typeElement = Objects.requireNonNull(segmentType.get(0));
        String segmentName = typeElement.getAttribute(TmfXmlStrings.SEGMENT_NAME);
        TmfXmlStateValueCu segmentTypeValue = null;
        if (!segmentName.isEmpty()) {
            // Constant name for the segment type
            segmentTypeValue = new TmfXmlStateValueCu(() -> new DataDrivenValueConstant(null, ITmfStateValue.Type.NULL, segmentName));
        } else {
            List<Element> nameElements = TmfXmlUtils.getChildElements(typeElement, TmfXmlStrings.SEGMENT_NAME);
            if (nameElements.size() != 1) {
                // TODO: Validation message here
                Activator.logError("Segment type: You need to either give a segName attribute or define one and only one <segName> element with child <stateValue> to get the type name"); //$NON-NLS-1$
                return null;
            }
            Element segNameElement = Objects.requireNonNull(nameElements.get(0));
            List<Element> nameValueElements = TmfXmlUtils.getChildElements(segNameElement, TmfXmlStrings.STATE_VALUE);
            if (nameValueElements.size() != 1) {
                // TODO: Validation message here
                Activator.logError("Segment type: the <segName> element should have one and only one <stateValue> child"); //$NON-NLS-1$
                return null;
            }
            Element segNameValueElement = Objects.requireNonNull(nameValueElements.get(0));
            segmentTypeValue = TmfXmlStateValueCu.compileValue(analysisData, segNameValueElement);
        }
        if (segmentTypeValue == null) {
            return null;
        }

        // Compile the optional time element
        TmfXmlStateValueCu beginTimeValue = null;
        TmfXmlStateValueCu durationValue = null;
        TmfXmlStateValueCu endTimeValue = null;
        List<Element> timeElements = TmfXmlUtils.getChildElements(node, TmfXmlStrings.SEGMENT_TIME);
        if (timeElements.size() > 1) {
            // TODO: Validation message here
            Activator.logWarning("Segment time: there should be only one <segTime> element"); //$NON-NLS-1$
        }
        if (timeElements.size() > 0) {
            Element segTimeElement = Objects.requireNonNull(timeElements.get(0));
            List<Element> beginElements = TmfXmlUtils.getChildElements(segTimeElement, TmfXmlStrings.BEGIN);
            if (beginElements.isEmpty()) {
                // TODO: Validation message here
                Activator.logError("Segment time: there should be one <begin> element to describe segment start time"); //$NON-NLS-1$
                return null;
            }
            if (beginElements.size() > 1) {
                // TODO: Validation message here
                Activator.logWarning("Segment time: there should be only one <begin> element"); //$NON-NLS-1$
            }
            beginTimeValue = TmfXmlStateValueCu.compileValue(analysisData, Objects.requireNonNull(beginElements.get(0)));
            if (beginTimeValue == null) {
                return null;
            }
            beginTimeValue = new TmfXmlStateValueCu(new TmfXmlStateValueCu.ValueWrapperGenerator(beginTimeValue, ITmfStateValue.Type.LONG));
            // Validate end time or duration
            List<Element> durationElements = TmfXmlUtils.getChildElements(segTimeElement, TmfXmlStrings.DURATION);
            List<Element> endElements = TmfXmlUtils.getChildElements(segTimeElement, TmfXmlStrings.END);
            if (durationElements.isEmpty() && endElements.isEmpty()) {
                // TODO: Validation message here
                Activator.logError("Segment time: there should be either a <duration> or <end> element"); //$NON-NLS-1$
                return null;
            }
            if (!durationElements.isEmpty() && !endElements.isEmpty()) {
                // TODO: Validation message here
                Activator.logError("Segment time: only one of <duration> or <end> shoud be present"); //$NON-NLS-1$
                return null;
            }
            if (durationElements.size() > 1) {
                // TODO: Validation message here
                Activator.logWarning("Segment time: there should be only one <duration> element"); //$NON-NLS-1$
            }
            if (endElements.size() > 1) {
                // TODO: Validation message here
                Activator.logWarning("Segment time: there should be only one <end> element"); //$NON-NLS-1$
            }
            if (!durationElements.isEmpty()) {
                durationValue = TmfXmlStateValueCu.compileValue(analysisData, Objects.requireNonNull(durationElements.get(0)));
                if (durationValue == null) {
                    return null;
                }
                durationValue = new TmfXmlStateValueCu(new TmfXmlStateValueCu.ValueWrapperGenerator(durationValue, ITmfStateValue.Type.LONG));
            } else {
                endTimeValue = TmfXmlStateValueCu.compileValue(analysisData, Objects.requireNonNull(endElements.get(0)));
                if (endTimeValue == null) {
                    return null;
                }
                endTimeValue = new TmfXmlStateValueCu(new TmfXmlStateValueCu.ValueWrapperGenerator(endTimeValue, ITmfStateValue.Type.LONG));
            }
        }

        // Compile the segment fields
        List<Element> contentElements = TmfXmlUtils.getChildElements(node, TmfXmlStrings.SEGMENT_CONTENT);
        Map<String, TmfXmlStateValueCu> fieldMap = new HashMap<>();
        for (Element contentEl : contentElements) {
            List<Element> fieldElements = TmfXmlUtils.getChildElements(contentEl, TmfXmlStrings.SEGMENT_FIELD);
            for (Element fieldEl : fieldElements) {
                String name = fieldEl.getAttribute(TmfXmlStrings.NAME);
                if (name.isEmpty()) {
                    // TODO: Validation message here
                    Activator.logError("Segment field does not have a name"); //$NON-NLS-1$
                    return null;
                }
                if (fieldMap.containsKey(name)) {
                    // TODO: Validation message here
                    Activator.logError("Redefinition of field " + name); //$NON-NLS-1$
                    return null;
                }
                TmfXmlStateValueCu fieldCu = TmfXmlStateValueCu.compileSegmentField(analysisData, fieldEl);
                if (fieldCu == null) {
                    return null;
                }
                fieldMap.put(name, fieldCu);
            }
        }

        return new TmfXmlActionSegmentCu(segmentTypeValue, beginTimeValue, durationValue, endTimeValue, fieldMap);
    }

    /**
     * Create an action from a list
     *
     * @param actions
     *            The list of actions
     * @return The new action
     */
    public static TmfXmlActionCu createActionList(List<TmfXmlActionCu> actions) {
        if (actions.size() == 0) {
            // There are unsupported actions, so the action list may be empty
            return NO_ACTION_CU;
        }
        return actions.size() == 1 ? actions.get(0) : new TmfXmlActionListCu(actions);
    }

}
