/*******************************************************************************
* Copyright (c) 2018 Ericsson
*
* 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.provisional.tmf.core.model.filter.parser;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.antlr.runtime.tree.CommonTree;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.tmf.core.filter.model.ITmfFilterTreeNode;
import org.eclipse.tracecompass.tmf.core.filter.model.TmfFilterAndNode;
import org.eclipse.tracecompass.tmf.core.filter.model.TmfFilterCompareNode;
import org.eclipse.tracecompass.tmf.core.filter.model.TmfFilterContainsNode;
import org.eclipse.tracecompass.tmf.core.filter.model.TmfFilterEqualsNode;
import org.eclipse.tracecompass.tmf.core.filter.model.TmfFilterMatchesNode;
import org.eclipse.tracecompass.tmf.core.filter.model.TmfFilterOrNode;
import org.eclipse.tracecompass.tmf.core.filter.model.TmfFilterTreeNode;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.filter.parser.FilterParserParser;

import com.google.common.collect.ImmutableList;

/**
 * This implements a filter expression compilation unit
 *
 * @author Jean-Christian Kouame
 *
 */
public class FilterExpressionCu implements IFilterCu {

    private List<Object> fElements;

    /**
     * Constructor
     *
     * @param elements
     *            The list of elements representing the logical expression
     */
    public FilterExpressionCu(List<Object> elements) {
        fElements = elements;
    }

    /**
     * Get the list of elements that define this logical expression
     *
     * @return the list of elements
     */
    public List<Object> getElement() {
        return fElements;
    }

    /**
     * Compile a filter expression from a tree
     *
     * @param treeNode
     *            The root node used to build this compilation unit
     * @return a filter expression compilation unit
     */
    public static @Nullable FilterExpressionCu compile(CommonTree treeNode) {
        int childCount = treeNode.getChildCount();

        // We can only have an odd number of parameter at this point
        if (childCount % 2 != 1) {
            return null;
        }

        List<Object> elements = new ArrayList<>();
        for (int i = 0; i < childCount; i++) {
            if (i % 2 == 0) {
                CommonTree nodeTree = (CommonTree) treeNode.getChild(i);
                int subChildCount = nodeTree.getChildCount();

                if (nodeTree.getType() == FilterParserParser.ROOT1) {
                    boolean negate = nodeTree.getChild(0).getText().equals(IFilterStrings.NOT);
                    // The logical part is the penultimate child
                    CommonTree logical = Objects.requireNonNull((CommonTree) nodeTree.getChild(subChildCount - 2));
                    FilterExpressionCu cu = negate ? FilterExpressionNotCu.compile(logical) : FilterExpressionCu.compile(logical);

                    if (cu == null) {
                        return null;
                    }

                    elements.add(cu);

                } else if (nodeTree.getType() == FilterParserParser.ROOT2) {
                    FilterSimpleExpressionCu node = FilterSimpleExpressionCu.compile(nodeTree);
                    if (node == null) {
                        return null;
                    }

                    elements.add(node);
                }
            } else {
                CommonTree opTree = (CommonTree) treeNode.getChild(i);
                String op = opTree.getText();
                elements.add(Objects.requireNonNull(op));
            }
        }

        return new FilterExpressionCu(elements);
    }

    /**
     * Generate a filter expression runtime object
     *
     * @return a filter expression
     */
    public FilterExpression generate() {
        List<Object> queue = getElementsQueue();
        return new FilterExpression(queue);
    }

    /**
     * get the queue of operations this expression validation will do
     *
     * @return The queue of operation
     */
    protected final List<Object> getElementsQueue() {
        ImmutableList.Builder<Object> builder = ImmutableList.builder();
        int count = fElements.size();

        for (int i = 0; i < count; i++) {
            if (i % 2 == 0) {
                Object element = fElements.get(i);
                if (element instanceof FilterSimpleExpressionCu) {

                    FilterSimpleExpression node = ((FilterSimpleExpressionCu) element).generate();

                    builder.add(node);
                } else if (element instanceof FilterExpressionCu) {

                    FilterExpression node = ((FilterExpressionCu) element).generate();

                    builder.add(node);

                } else {
                    throw new IllegalStateException("Unknown element while getting the filter element queue"); //$NON-NLS-1$
                }
            } else {
                Object op = fElements.get(i);
                if (!(op instanceof String)) {
                    throw new IllegalStateException("Element at position " + i + " should be a String"); //$NON-NLS-1$ //$NON-NLS-2$
                }
                builder.add(op);
            }
        }
        return builder.build();
    }

    @Override
    public ITmfFilterTreeNode getEventFilter(ITmfTrace trace) {
        int count = fElements.size();
        ITmfFilterTreeNode prevNode = null;
        String operator = IFilterStrings.OR;

        for (int i = 0; i < count; i++) {
            if (i % 2 == 0) {
                Object element = fElements.get(i);
                if (element instanceof IFilterCu) {

                    ITmfFilterTreeNode node = ((IFilterCu) element).getEventFilter(trace);
                    prevNode = mergeEventFilters(prevNode, operator, node);
                } else {
                    throw new IllegalStateException("Unknown element while getting the filter element queue"); //$NON-NLS-1$
                }
            } else {
                Object op = fElements.get(i);
                if (!(op instanceof String)) {
                    throw new IllegalStateException("Element at position " + i + " should be a String"); //$NON-NLS-1$ //$NON-NLS-2$
                }
                operator = (String) op;
            }
        }
        if (prevNode == null) {
            throw new NullPointerException("The filter should not be null at this point"); //$NON-NLS-1$
        }
        if (getNot()) {
            setFilterNot(prevNode);
        }
        return prevNode;
    }

    private static void setFilterNot(ITmfFilterTreeNode node) {
        // TODO: Ideally all those classes would have a common parent class, but
        // that would be API breaking
        if (node instanceof TmfFilterOrNode) {
            ((TmfFilterOrNode) node).setNot(true);
        }
        if (node instanceof TmfFilterAndNode) {
            ((TmfFilterAndNode) node).setNot(true);
        }
        if (node instanceof TmfFilterContainsNode) {
            ((TmfFilterContainsNode) node).setNot(true);
        }
        if (node instanceof TmfFilterEqualsNode) {
            ((TmfFilterEqualsNode) node).setNot(true);
        }
        if (node instanceof TmfFilterCompareNode) {
            ((TmfFilterCompareNode) node).setNot(true);
        }
        if (node instanceof TmfFilterMatchesNode) {
            ((TmfFilterMatchesNode) node).setNot(true);
        }
    }

    private ITmfFilterTreeNode mergeEventFilters(@Nullable ITmfFilterTreeNode prevNode, String operator, ITmfFilterTreeNode node) {
        if (prevNode == null) {
            return node;
        }
        TmfFilterTreeNode parentNode;
        if (operator == IFilterStrings.OR) {
            TmfFilterOrNode orNode = new TmfFilterOrNode(null);
            orNode.setNot(getNot());
            parentNode = orNode;
        } else {
            TmfFilterAndNode andNode = new TmfFilterAndNode(null);
            andNode.setNot(getNot());
            parentNode = andNode;
        }
        parentNode.addChild(prevNode);
        parentNode.addChild(node);

        return parentNode;
    }

    /**
     * Get whether this Cu expression is a negation
     *
     * @return <code>true</code> if the expression is a negation
     */
    protected boolean getNot() {
        return false;
    }

}
