/*******************************************************************************
 * 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.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

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.DataDrivenCondition;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenCondition.ConditionOperator;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.fsm.model.DataDrivenCondition.TimeRangeOperator;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.module.XmlUtils;
import org.eclipse.tracecompass.tmf.analysis.xml.core.module.TmfXmlStrings;
import org.eclipse.tracecompass.tmf.analysis.xml.core.module.TmfXmlUtils;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.w3c.dom.Element;

/**
 * The base compilation unit for XML tests and conditions
 *
 * @author Geneviève Bastien
 * @author Florian Wininger
 */
public abstract class TmfXmlConditionCu implements IDataDrivenCompilationUnit {


    private static final TmfXmlConditionCu TRUE_CONDITION = new TmfXmlConditionCu() {

        @Override
        public DataDrivenCondition generate() {
            return DataDrivenCondition.TRUE_CONDITION;
        }

    };

    /** Compararison condition */
    private static class TmfXmlCompareConditionCu extends TmfXmlConditionCu {

        private final TmfXmlStateValueCu fFirstValue;
        private final TmfXmlStateValueCu fSecondValue;
        private final ConditionOperator fOperator;

        TmfXmlCompareConditionCu(TmfXmlStateValueCu firstValue, TmfXmlStateValueCu secondValue, ConditionOperator conditionOperator) {
            fFirstValue = firstValue;
            fSecondValue = secondValue;
            fOperator = conditionOperator;
        }

        @Override
        public DataDrivenCondition generate() {
            return new DataDrivenCondition.DataDrivenComparisonCondition(fFirstValue.generate(), fSecondValue.generate(), fOperator);
        }

    }

    /** Compare time range */
    private static class TmfXmlTimeRangeConditionCu extends TmfXmlConditionCu {

        private final TimeRangeOperator fOperator;
        private final long fBegin;
        private final long fEnd;

        TmfXmlTimeRangeConditionCu(TimeRangeOperator operator, long begin, long end) {
            fOperator = operator;
            fBegin = begin;
            fEnd = end;
        }

        @Override
        public DataDrivenCondition generate() {
            return new DataDrivenCondition.DataDrivenTimeRangeCondition(fOperator, fBegin, fEnd);
        }

    }

    /** Compare time range */
    private static class TmfXmlElapsedTimeConditionCu extends TmfXmlConditionCu {

        private final ConditionOperator fOperator;
        private final String fReference;
        private final long fValue;

        TmfXmlElapsedTimeConditionCu(ConditionOperator operator, String reference, long end) {
            fOperator = operator;
            fReference = reference;
            fValue = end;
        }

        @Override
        public DataDrivenCondition generate() {
            return new DataDrivenCondition.DataDrivenElapsedTimeCondition(fOperator, fReference, fValue);
        }

    }

    /** NOT condition */
    private static class TmfXmlNotConditionCu extends TmfXmlConditionCu {

        private final TmfXmlConditionCu fCondition;

        TmfXmlNotConditionCu(TmfXmlConditionCu compile) {
            fCondition = compile;
        }

        @Override
        public DataDrivenCondition generate() {
            return new DataDrivenCondition.DataDrivenNotCondition(fCondition.generate());
        }

    }

    /** Pattern matching condition */
    static class TmfXmlRegexConditionCu extends TmfXmlConditionCu {

        private final Pattern fPattern;
        private final TmfXmlStateValueCu fValue;

        TmfXmlRegexConditionCu(Pattern pattern, TmfXmlStateValueCu value) {
            fPattern = pattern;
            fValue = value;
        }

        @Override
        public DataDrivenCondition generate() {
            return new DataDrivenCondition.DataDrivenRegexCondition(fPattern, fValue.generate());
        }

    }

    /** AND condition */
    public static class TmfXmlAndConditionCu extends TmfXmlConditionCu {

        private final List<TmfXmlConditionCu> fConditions;

        TmfXmlAndConditionCu(List<TmfXmlConditionCu> childConditions) {
            fConditions = childConditions;
        }

        @Override
        public DataDrivenCondition generate() {
            List<DataDrivenCondition> conditions = fConditions.stream()
                    .map(TmfXmlConditionCu::generate)
                    .collect(Collectors.toList());
            return new DataDrivenCondition.DataDrivenAndCondition(conditions);
        }

    }

    /** OR condition */
    public static class TmfXmlOrConditionCu extends TmfXmlConditionCu {

        private final List<TmfXmlConditionCu> fConditions;

        TmfXmlOrConditionCu(List<TmfXmlConditionCu> childConditions) {
            fConditions = childConditions;
        }

        @Override
        public DataDrivenCondition generate() {
            List<DataDrivenCondition> conditions = fConditions.stream()
                    .map(TmfXmlConditionCu::generate)
                    .collect(Collectors.toList());
            return new DataDrivenCondition.DataDrivenOrCondition(conditions);
        }

    }

    @Override
    public abstract DataDrivenCondition generate();

    /**
     * Compile a test element, ie an element that contains a name
     *
     * TODO: Return the condition 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 condition
     * @return The condition
     */
    public static @Nullable TmfXmlConditionCu compileNamedCondition(AnalysisCompilationData analysisData, Element namedEl) {
        String id = namedEl.getAttribute(TmfXmlStrings.ID);

        List<@Nullable Element> childElements = XmlUtils.getChildElements(namedEl);
        Element child = NonNullUtils.checkNotNull(childElements.get(0));
        // Compile the child of the IF node
        childElements = XmlUtils.getChildElements(child);
        if (childElements.size() != 1) {
            // TODO: Validation message here
            Activator.logError("There should be only one element under this condition"); //$NON-NLS-1$
            throw new NullPointerException("Can't compile the condition"); //$NON-NLS-1$
        }
        Element subCondition = Objects.requireNonNull(childElements.get(0));

        TmfXmlConditionCu condition = compile(analysisData, subCondition);
        if (condition == null) {
            return null;
        }
        analysisData.addTest(id, condition);
        return condition;
    }

    /**
     * @param analysisData
     *            The analysis data already compiled
     * @param conditionEl
     *            the XML element corresponding to the condition
     * @return The condition compilation unit or <code>null</code> if there was
     *         compilation error
     */
    public static @Nullable TmfXmlConditionCu compile(AnalysisCompilationData analysisData, Element conditionEl) {
        switch (conditionEl.getNodeName()) {
        case TmfXmlStrings.CONDITION:
            return compileSingleCondition(analysisData, conditionEl);
        case TmfXmlStrings.NOT: {
            List<@Nullable Element> childElements = XmlUtils.getChildElements(conditionEl);
            if (childElements.size() != 1) {
                Activator.logError("Compiling condition: NOT condition must have 1 and only 1 child"); //$NON-NLS-1$
                return null;
            }
            Element element = Objects.requireNonNull(childElements.get(0));
            TmfXmlConditionCu compile = compile(analysisData, element);
            return (compile == null ? null : new TmfXmlNotConditionCu(compile));
        }
        case TmfXmlStrings.AND: {
            List<TmfXmlConditionCu> childConditions = getCompiledChildConditions(analysisData, conditionEl);
            return (childConditions == null ? null : new TmfXmlAndConditionCu(childConditions));
        }
        case TmfXmlStrings.OR: {
            List<TmfXmlConditionCu> childConditions = getCompiledChildConditions(analysisData, conditionEl);
            return (childConditions == null ? null : new TmfXmlOrConditionCu(childConditions));
        }
        default:
            Activator.logError("Xml condition: Unsupported condition type: " + conditionEl.getNodeName()); //$NON-NLS-1$
        }
        return null;
    }

    private static @Nullable List<TmfXmlConditionCu> getCompiledChildConditions(AnalysisCompilationData analysisData, Element conditionEl) {
        List<@Nullable Element> childElements = XmlUtils.getChildElements(conditionEl);
        if (childElements.isEmpty()) {
            Activator.logError("Compiling condition: AND and OR condition must have at least 1 element"); //$NON-NLS-1$
            return null;
        }
        List<TmfXmlConditionCu> childConditions = new ArrayList<>();
        for (Element element : childElements) {
            TmfXmlConditionCu condition = compile(analysisData, Objects.requireNonNull(element));
            if (condition == null) {
                return null;
            }
            childConditions.add(condition);
        }
        return childConditions;
    }

    private static @Nullable TmfXmlConditionCu compileSingleCondition(AnalysisCompilationData analysisData, Element conditionEl) {
        // Is the condition a comparison condition?
        if (conditionEl.getElementsByTagName(TmfXmlStrings.STATE_VALUE).getLength() > 0) {
            return compileValueCondition(analysisData, conditionEl);
        }
        // Compile a time range condition
        List<Element> childElements = TmfXmlUtils.getChildElements(conditionEl, TmfXmlStrings.TIME_RANGE);
        if (childElements.size() == 1) {
            return compileTimeRangeCondition(childElements.get(0));
        }
        // Compile an elapsed time condition
        childElements = TmfXmlUtils.getChildElements(conditionEl, TmfXmlStrings.ELAPSED_TIME);
        if (childElements.size() == 1) {
            return compileElapsedTimeCondition(childElements.get(0));
        }
        return null;
    }

    private static @Nullable TmfXmlConditionCu compileElapsedTimeCondition(Element element) {
        String unit = element.getAttribute(TmfXmlStrings.UNIT);
        List<@Nullable Element> childElements = XmlUtils.getChildElements(element);
        if (childElements.size() != 1) {
            Activator.logError("Invalid timestampsChecker declaration in XML : Only one timing condition is allowed"); //$NON-NLS-1$
            return null;
        }
        final Element firstElement = NonNullUtils.checkNotNull(childElements.get(0));
        String type = firstElement.getNodeName();
        ConditionOperator operator;
        switch (type) {
        case TmfXmlStrings.LESS:
            operator = ConditionOperator.LT;
            break;
        case TmfXmlStrings.EQUAL:
            operator = ConditionOperator.EQ;
            break;
        case TmfXmlStrings.MORE:
            operator = ConditionOperator.GT;
            break;
        default:
            Activator.logError("ElapsedTimeChecker: Invalid operator: " + type); //$NON-NLS-1$
            return null;
        }
        final String reference = firstElement.getAttribute(TmfXmlStrings.SINCE);
        final String valueStr = firstElement.getAttribute(TmfXmlStrings.VALUE);
        try {
            long value = valueToNanoseconds(Long.parseLong(valueStr), unit);
            return new TmfXmlElapsedTimeConditionCu(operator, reference, value);
        } catch (NumberFormatException e) {
            Activator.logError("Invalid value for elapsed time: " + e.getMessage()); //$NON-NLS-1$
            return null;
        }
    }

    private static @Nullable TmfXmlConditionCu compileTimeRangeCondition(Element element) {
        String unit = element.getAttribute(TmfXmlStrings.UNIT);

        List<@Nullable Element> childElements = NonNullUtils.checkNotNull(XmlUtils.getChildElements(element));
        if (childElements.size() != 1) {
            Activator.logError("Invalid timestampsChecker declaration in XML : Only one timing condition is allowed"); //$NON-NLS-1$
            return null;
        }
        final Element firstElement = NonNullUtils.checkNotNull(childElements.get(0));
        TimeRangeOperator operator;
        String type = firstElement.getNodeName();
        switch (type) {
        case TmfXmlStrings.IN:
            operator = TimeRangeOperator.IN;
            break;
        case TmfXmlStrings.OUT:
            operator = TimeRangeOperator.OUT;
            break;
        default:
            Activator.logError("TimeRangeChecker: Invalid operator: " + type); //$NON-NLS-1$
            return null;
        }

        final String beginStr = firstElement.getAttribute(TmfXmlStrings.BEGIN);
        final String endStr = firstElement.getAttribute(TmfXmlStrings.END);
        try {
            long begin = valueToNanoseconds(Long.parseLong(beginStr), unit);
            long end = valueToNanoseconds(Long.parseLong(endStr), unit);

            return new TmfXmlTimeRangeConditionCu(operator, begin, end);
        } catch (NumberFormatException e) {
            Activator.logError("Invalid value for time range: " + e.getMessage()); //$NON-NLS-1$
            return null;
        }
    }

    /**
     * Normalize the value into a nanosecond time value
     *
     * @param timestamp
     *            The timestamp value
     * @param unit
     *            The initial unit of the timestamp
     * @return The value of the timestamp in nanoseconds
     */
    public static long valueToNanoseconds(long timestamp, String unit) {
        switch (unit) {
        case TmfXmlStrings.NS:
            return timestamp;
        case TmfXmlStrings.US:
            return TmfTimestamp.create(timestamp, ITmfTimestamp.MICROSECOND_SCALE).toNanos();
        case TmfXmlStrings.MS:
            return TmfTimestamp.create(timestamp, ITmfTimestamp.MILLISECOND_SCALE).toNanos();
        case TmfXmlStrings.S:
            return TmfTimestamp.create(timestamp, ITmfTimestamp.SECOND_SCALE).toNanos();
        default:
            throw new IllegalArgumentException("The time unit is not yet supporting."); //$NON-NLS-1$
        }
    }

    private static @Nullable TmfXmlConditionCu compileValueCondition(AnalysisCompilationData analysisData, Element conditionEl) {
        ConditionOperator conditionOperator = getConditionOperator(conditionEl);
        // Compile the case with 2 state values
        List<Element> childElements = TmfXmlUtils.getChildElements(conditionEl, TmfXmlStrings.STATE_VALUE);
        if (childElements.size() == 2) {
            TmfXmlStateValueCu firstValue = TmfXmlStateValueCu.compileValue(analysisData, childElements.get(0));
            TmfXmlStateValueCu secondValue = TmfXmlStateValueCu.compileValue(analysisData, childElements.get(1));
            if (firstValue == null || secondValue == null) {
                return null;
            }
            return new TmfXmlCompareConditionCu(firstValue, secondValue, conditionOperator);
        }
        // Compile the case with first element being a stateAttribute or event
        // field
        if (childElements.size() == 1) {
            TmfXmlStateValueCu secondValue = TmfXmlStateValueCu.compileValue(analysisData, childElements.get(0));
            TmfXmlStateValueCu firstValue = null;
            List<Element> attributes = TmfXmlUtils.getChildElements(conditionEl, TmfXmlStrings.STATE_ATTRIBUTE);
            if (!attributes.isEmpty()) {
                // The first value is an array of state attributes
                TmfXmlStateSystemPathCu path = TmfXmlStateSystemPathCu.compile(analysisData, attributes);
                if (path == null) {
                    return null;
                }
                firstValue = TmfXmlStateValueCu.compileAsQuery(path);
            } else {
                // The first value is an event field
                attributes = TmfXmlUtils.getChildElements(conditionEl, TmfXmlStrings.ELEMENT_FIELD);
                if (attributes.size() != 1) {
                    Activator.logError("Condition: There should be either 2 state values or 1 attribute or field and 1 state value"); //$NON-NLS-1$
                    return null;
                }
                firstValue = TmfXmlStateValueCu.compileField(analysisData, attributes.get(0));
            }
            if (firstValue == null || secondValue == null) {
                return null;
            }
            return new TmfXmlCompareConditionCu(firstValue, secondValue, conditionOperator);
        }
        return null;
    }

    private static DataDrivenCondition.ConditionOperator getConditionOperator(Element rootNode) {
        String equationType = rootNode.getAttribute(TmfXmlStrings.OPERATOR);
        switch (equationType) {
        case TmfXmlStrings.EQ:
            return ConditionOperator.EQ;
        case TmfXmlStrings.NE:
            return ConditionOperator.NE;
        case TmfXmlStrings.GE:
            return ConditionOperator.GE;
        case TmfXmlStrings.GT:
            return ConditionOperator.GT;
        case TmfXmlStrings.LE:
            return ConditionOperator.LE;
        case TmfXmlStrings.LT:
            return ConditionOperator.LT;
        case TmfXmlStrings.NULL:
            return ConditionOperator.EQ;
        default:
            throw new IllegalArgumentException("TmfXmlCondition: invalid comparison operator."); //$NON-NLS-1$
        }
    }

    /**
     * Create an OR condition from the list of conditions, or return the single
     * condition if there is only one
     *
     * @param conditions
     *            The list of conditions to OR
     * @return And OR condition, or the single condition
     */
    public static TmfXmlConditionCu createOrCondition(List<TmfXmlConditionCu> conditions) {
        if (conditions.isEmpty()) {
            return TRUE_CONDITION;
        }
        return conditions.size() == 1 ? conditions.get(0) : new TmfXmlOrConditionCu(conditions);
    }

    /**
     * Create an AND condition from the list of conditions, or return the single
     * condition if there is only one
     *
     * @param conditions
     *            The list of conditions to AND
     * @return And AND condition, or the single condition
     */
    public static @Nullable TmfXmlConditionCu createAndCondition(List<TmfXmlConditionCu> conditions) {
        if (conditions.isEmpty()) {
            return TRUE_CONDITION;
        }
        return conditions.size() == 1 ? conditions.get(0) : new TmfXmlAndConditionCu(conditions);
    }

}
