/*******************************************************************************
 * Copyright (c) 2015 É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.statesystem.core.tests.stubs.backend;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.channels.ClosedChannelException;
import java.util.Collection;
import java.util.List;

import org.eclipse.tracecompass.internal.statesystem.core.Activator;
import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.HTConfig;
import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.HTNode;
import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.IHistoryTree;
import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.ParentNode;
import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.classic.CoreNode;
import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.classic.HistoryTreeClassic;

import com.google.common.collect.Iterables;

/**
 * Stub class to unit test the history tree. You can set the size of the
 * interval section before using the tree, in order to fine-tune the test.
 *
 * Note to developers: This tree is not meant to be used with a backend. It just
 * exposes some info from the history tree.
 *
 * @author Geneviève Bastien
 */
public class HistoryTreeClassicStub extends HistoryTreeClassic {

    /**
     * Minimum size a block of this tree should have
     */
    public static final int MINIMUM_BLOCK_SIZE = IHistoryTree.TREE_HEADER_SIZE;

    /**
     * Constructor for this history tree stub
     *
     * @param conf
     *            The config to use for this History Tree.
     * @throws IOException
     *             If an error happens trying to open/write to the file
     *             specified in the config
     */
    public HistoryTreeClassicStub(HTConfig conf) throws IOException {
        super(conf);
    }

    /**
     * "Reader" constructor : instantiate a SHTree from an existing tree file on
     * disk
     *
     * @param existingStateFile
     *            Path/filename of the history-file we are to open
     * @param expProviderVersion
     *            The expected version of the state provider
     * @throws IOException
     *             If an error happens reading the file
     */
    public HistoryTreeClassicStub(File existingStateFile, int expProviderVersion) throws IOException {
        super(existingStateFile, expProviderVersion);
    }

    // ------------------------------------------------------------------------
    // Extra test accessors
    // ------------------------------------------------------------------------

    @Override
    public List<HTNode> getLatestBranch() {
        /* Super method is not public */
        return checkNotNull(super.getLatestBranch());
    }

    /**
     * Get the latest leaf of the tree
     *
     * @return The current leaf node of the tree
     */
    public HTNode getLatestLeaf() {
        List<HTNode> latest = getLatestBranch();
        return Iterables.getLast(latest);
    }

    /**
     * Get the node from the latest branch at a given position, 0 being the root
     * and <size of latest branch - 1> being a leaf node.
     *
     * @param pos
     *            The position at which to return the node
     * @return The node at position pos
     */
    public HTNode getNodeAt(int pos) {
        List<HTNode> latest = getLatestBranch();
        return latest.get(pos);
    }

    /**
     * Get the depth of the tree
     *
     * @return The depth of the tree
     */
    public int getDepth() {
        return getLatestBranch().size();
    }

    // ------------------------------------------------------------------------
    // Debug printing methods
    // ------------------------------------------------------------------------

    /**
     * Print out the full tree for debugging purposes
     *
     * @param writer
     *            PrintWriter in which to write the output
     * @param printIntervals
     *            Flag to enable full output of the interval information
     * @param ts
     *            The timestamp that nodes have to intersect for intervals to be
     *            printed. A negative value will print intervals for all nodes.
     *            The timestamp only applies if printIntervals is true.
     */
    public void debugPrintFullTree(PrintWriter writer, boolean printIntervals, long ts) {
        preOrderPrint(writer, false, getLatestBranch().get(0), 0, ts);

        if (printIntervals) {
            preOrderPrint(writer, true, getLatestBranch().get(0), 0, ts);
        }
        writer.println('\n');
    }

    /**
     * Start at currentNode and print the contents of all its children, in
     * pre-order. Give the root node in parameter to visit the whole tree, and
     * have a nice overview.
     */
    private void preOrderPrint(PrintWriter writer, boolean printIntervals,
            HTNode currentNode, int curDepth, long ts) {

        writer.println(currentNode.toString());
        /*
         * Print intervals only if timestamp is negative or within the range of
         * the node
         */
        if (printIntervals &&
                (ts <= 0 ||
                        (ts >= currentNode.getNodeStart() && ts <= currentNode.getNodeEnd()))) {
            currentNode.debugPrintIntervals(writer);
        }

        switch (currentNode.getNodeType()) {
        case LEAF:
            /* Stop if it's the leaf node */
            return;

        case CORE:
            try {
                final CoreNode node = (CoreNode) currentNode;
                /* If node extensions were used, they would be printed here. */

                /* Print the child nodes */
                for (int i = 0; i < node.getNbChildren(); i++) {
                    HTNode nextNode = getTreeIO().readNode(node.getChild(i));
                    for (int j = 0; j < curDepth; j++) {
                        writer.print("  ");
                    }
                    writer.print("+-");
                    preOrderPrint(writer, printIntervals, nextNode, curDepth + 1, ts);
                }
            } catch (ClosedChannelException e) {
                Activator.getDefault().logError(e.getMessage());
            }
            break;

        default:
            break;
        }
    }

    // ------------------------------------------------------------------------
    // Assertion methods, for use with JUnit tests
    // ------------------------------------------------------------------------

    /**
     * Check the integrity of all the nodes in the tree. Calls
     * {@link #assertNodeIntegrity} for every node in the tree.
     */
    public void assertIntegrity() {
        try {
            for (int i = 0; i < getNodeCount(); i++) {
                assertNodeIntegrity(getNode(i));
            }
        } catch (ClosedChannelException e) {
            fail(e.getMessage());
        }
    }

    /**
     * Debugging method to make sure all intervals contained in the given node
     * have valid start and end times.
     *
     * @param node
     *            The node to check
     */
    private void assertNodeIntegrity(HTNode node) {
        if (node instanceof ParentNode) {
            assertChildrenIntegrity((ParentNode) node);
        }

        /* Check that all intervals are within the node's range */
        // TODO: Get the intervals of a node

    }

    private void assertChildrenIntegrity(ParentNode node) {
        try {
            /*
             * Test that this node's start and end times match the start of the
             * first child and the end of the last child, respectively
             */
            if (node.getNbChildren() > 0) {
                HTNode childNode = getNode(node.getChild(0));
                assertEquals("Equals start time of parent " + node.getSequenceNumber() + " and first child " + childNode.getSequenceNumber(),
                        node.getNodeStart(), childNode.getNodeStart());
                if (node.isOnDisk()) {
                    childNode = getNode(node.getLatestChild());
                    assertEquals("Equals end time of parent " + node.getSequenceNumber() + " and last child " + childNode.getSequenceNumber(),
                            node.getNodeEnd(), childNode.getNodeEnd());
                }
            }

            /*
             * Test that the childStartTimes[] array matches the real nodes'
             * start times
             *
             * Also test that children range is within the parent's range
             */
            for (int i = 0; i < node.getNbChildren(); i++) {
                HTNode childNode = getNode(node.getChild(i));
                assertEquals("Start time in parent " + node.getSequenceNumber() + " of child at index " + i,
                        childNode.getNodeStart(), node.getChildStart(i));
                assertTrue("Child at index " + i + " of parent " + node.getSequenceNumber() + " has correct start time",
                        node.getNodeStart() <= childNode.getNodeStart());
                if (node.isOnDisk() && childNode.isOnDisk()) {
                    assertTrue("Child at index " + i + " of parent " + node.getSequenceNumber() + " has correct end time",
                            childNode.getNodeEnd() <= node.getNodeEnd());
                    assertEquals("End time in parent " + node.getSequenceNumber() + " of child at index " + i,
                            childNode.getNodeEnd(), node.getChildEnd(i));
                }
                testIntersectingChildren(node, childNode);
            }

        } catch (ClosedChannelException e) {
            fail(e.getMessage());
        }
    }

    private static void testIntersectingChildren(ParentNode parent, HTNode child) {
        int childSequence = child.getSequenceNumber();
        boolean shouldBeInCollection;
        Collection<Integer> nextChildren;
        for (long t = parent.getNodeStart(); t < parent.getNodeEnd(); t++) {
            shouldBeInCollection = (t >= child.getNodeStart() && t <= child.getNodeEnd());
            nextChildren = parent.selectNextChildren(t);
            assertEquals(shouldBeInCollection, nextChildren.contains(childSequence));
        }
    }

}
