/*******************************************************************************
 * Copyright (c) 2016 É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.perf.historytree;

import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;

import org.apache.commons.io.FileUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.test.performance.Dimension;
import org.eclipse.test.performance.Performance;
import org.eclipse.test.performance.PerformanceMeter;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.HistoryTreeBackend;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystemBuilder;
import org.eclipse.tracecompass.statesystem.core.StateSystemFactory;
import org.eclipse.tracecompass.statesystem.core.StateSystemUtils;
import org.eclipse.tracecompass.statesystem.core.backend.IStateHistoryBackend;
import org.eclipse.tracecompass.statesystem.core.backend.StateHistoryBackendFactory;
import org.eclipse.tracecompass.statesystem.core.exceptions.AttributeNotFoundException;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateValueTypeException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import com.google.common.collect.ImmutableList;

/**
 * This class benchmarks writing intervals to the state system and querying them
 * using a history tree backend
 *
 * @author Geneviève Bastien
 */
@RunWith(Parameterized.class)
public class HistoryTreeBackendBenchmark {

    private static final @NonNull String TEST_PREFIX = "org.eclipse.tracecompass#History Tree Backend#";
    private static final @NonNull String TEST_BUILDING_ID = "Build: ";
    private static final @NonNull String TEST_SINGLE_QUERY_ID = "Single Queries: ";
    private static final @NonNull String TEST_FULL_QUERY_ID = "Full Queries: ";
    private static final @NonNull String TEST_QUERY_RANGE_ID = "Query History Range: ";
    private static final @NonNull String TEST_2D_QUERY_ID = "2D Queries: ";
    private static final @NonNull String TEST_REVERSE_2D_QUERY_ID = "Reverse 2D Queries: ";
    private static final @NonNull String ROOT_NODE = "root";
    private static final int QUEUE_SIZE = 10000;
    private static final long SEED = 5575784704147L;
    private static final int QUERY_COUNT = 100;
    private static final int INTERVAL_AVG_TIME = 1000;

    /* Values for the average case */
    private static final int DEFAULT_NB_ATTRIB = 1500;
    private static final int DEFAULT_NB_INTERVALS = 500;
    private static final int DEFAULT_LOOP_COUNT = 40;

    private File fTempFile;
    private final String fName;
    private final String fShortName;
    private final int fNbAttrib;
    private final int fNbAvgIntervals;
    private final int fNbLoops;
    private final HTBValues fValues;
    private final IIntervalDistribution fDistributionMethod;

    /**
     * Constructor
     *
     * @param name
     *            The name of the test
     * @param shortName
     *            A short name for this scenario (at most 40 characters,
     *            otherwise it will be truncated in the DB)
     * @param nbAttrib
     *            The number of attributes
     * @param nbAvgIntervals
     *            An idea on the number of intervals per attributes. The exact
     *            number will depend on the interval time distribution
     * @param nbLoops
     *            The number of times to execute the benchmark
     * @param values
     *            The values to put in the history tree
     * @param distributionMethod
     *            A distribution method that will return the next interval
     *            duration according to an algorithm
     */
    public HistoryTreeBackendBenchmark(String name, String shortName, int nbAttrib, int nbAvgIntervals, int nbLoops, HTBValues values, IIntervalDistribution distributionMethod) {
        fName = name;
        fShortName = shortName;
        fNbAttrib = nbAttrib;
        fNbAvgIntervals = nbAvgIntervals;
        fNbLoops = nbLoops;
        fValues = values;
        fDistributionMethod = distributionMethod;
    }

    /* A list of values to use for the intervals */
    private enum HTBValues {
        INTEGERS(ImmutableList.of(1, 2, 3)),
        STRINGS(ImmutableList.of("abc", "def", "wihi!")),
        LONGS(ImmutableList.of(Long.MAX_VALUE, 1L, 1234567L)),
        DOUBLES(ImmutableList.of(Double.MAX_VALUE, 1.0, 123.456));

        private final List<Object> fValues;

        private HTBValues(List<Object> values) {
            fValues = values;
        }

        public List<Object> getValues() {
            return fValues;
        }
    }

    @FunctionalInterface
    private static interface IIntervalDistribution {
        long getNextEndTime(Random randomGenerator, long limit);
    }

    /* Generate interval duration uniformly distributed in the range [1, 2l] */
    private static IIntervalDistribution UNIFORM = (r, l) -> {
        long nextLong = r.nextLong();
        long nextDelta = l + (nextLong % l);
        return nextDelta;
    };

    /*
     * Generates interval duration in the range [1, 2l] with 75% between [0.5l,
     * 1.5l]
     */
    private static IIntervalDistribution CLOSER_TO_LIMIT = (r, l) -> {
        /*
         * This method will return interval time closer to the specified limit
         */
        double nextDouble = r.nextDouble();
        int sign = (nextDouble < 0) ? -1 : 1;
        long nextDelta = l + (long) (Math.sqrt(nextDouble) * l * sign);
        return nextDelta;
    };

    /*
     * Generates interval duration in the range [1, 2l] with 75% between [0.5l,
     * 1.5l], but with 10% outliers between [2l, 5l]
     */
    private static IIntervalDistribution CLOSER_TO_LIMIT_10_PERCENT_OUTLIERS = (r, l) -> {
        long nextLong = Math.abs(r.nextLong());
        /* Is this an outlier */
        if ((nextLong % 100) < 10) {
            /* Create an outlier that can go up to 5 times the limit */
            return l + (((nextLong % 3) + 1) * l) + (nextLong % l);
        }
        /* Otherwise follow a distribution with more values around the limit */
        double nextDouble = r.nextDouble();
        int sign = (nextDouble < 0) ? -1 : 1;
        long nextDelta = l + (long) (Math.sqrt(nextDouble) * l * sign);
        return nextDelta;
    };

    /**
     * @return The arrays of parameters
     */
    @Parameters(name = "{index}: {0}")
    public static Iterable<Object[]> getParameters() {
        return Arrays.asList(new Object[][] {
                { "Average case: 1500 attributes, integers, interval duration random around limit l with 75 percent within [0.5l, 1.5l]", "Average case", DEFAULT_NB_ATTRIB, DEFAULT_NB_INTERVALS, DEFAULT_LOOP_COUNT, HTBValues.INTEGERS, CLOSER_TO_LIMIT },
                { "Vertical scaling (more attributes)", "Vertical scaling", 3500, DEFAULT_NB_INTERVALS, 5, HTBValues.INTEGERS, CLOSER_TO_LIMIT },
                { "Horizontal scaling (more intervals/attribute)", "Horizontal scaling", DEFAULT_NB_ATTRIB, 20000, 10, HTBValues.INTEGERS, CLOSER_TO_LIMIT },
                { "Interval durations uniformly distributed within [1, 2l]", "Uniform distribution of intervals", DEFAULT_NB_ATTRIB, DEFAULT_NB_INTERVALS, DEFAULT_LOOP_COUNT, HTBValues.INTEGERS, UNIFORM },
                { "Interval durations with 10 percent outliers > 2l", "Distribution with outliers", DEFAULT_NB_ATTRIB, DEFAULT_NB_INTERVALS, DEFAULT_LOOP_COUNT, HTBValues.INTEGERS, CLOSER_TO_LIMIT_10_PERCENT_OUTLIERS },
                { "Data type: strings", "Data type: strings", DEFAULT_NB_ATTRIB, DEFAULT_NB_INTERVALS, DEFAULT_LOOP_COUNT, HTBValues.STRINGS, CLOSER_TO_LIMIT },
                { "Data type: longs", "Data type: longs", DEFAULT_NB_ATTRIB, DEFAULT_NB_INTERVALS, DEFAULT_LOOP_COUNT, HTBValues.LONGS, CLOSER_TO_LIMIT },
                { "Data type: doubles", "Data type: doubles", DEFAULT_NB_ATTRIB, DEFAULT_NB_INTERVALS, DEFAULT_LOOP_COUNT, HTBValues.DOUBLES, CLOSER_TO_LIMIT },
        });
    }

    /**
     * Create the temporary file for this history tree
     */
    private void createFile() {
        try {
            fTempFile = File.createTempFile("tmpStateSystem", null);
        } catch (IOException e) {
            fail(e.getMessage());
        }
    }

    /**
     * Delete the temporary history tree file after the test
     */
    private void deleteFile() {
        if (fTempFile != null) {
            fTempFile.delete();
            fTempFile = null;
        }
    }

    private static class QuarkEvent implements Comparable<QuarkEvent> {
        private final int fQuark;
        private long fNextEventTime;
        private final List<Object> fPossibleValues;
        private int fNextValue = 0;

        public QuarkEvent(int quark, long nextEventTime, List<Object> valuesList) {
            fQuark = quark;
            fNextEventTime = nextEventTime;
            fPossibleValues = valuesList;
        }

        public int getQuark() {
            return fQuark;
        }

        public long getNextEventTime() {
            return fNextEventTime;
        }

        public void setNextEventTime(long t) {
            fNextEventTime = t;
        }

        public Object getNextValue() {
            Object value = fPossibleValues.get(fNextValue);
            fNextValue++;
            if (fNextValue >= fPossibleValues.size()) {
                fNextValue = 0;
            }
            return value;
        }

        @Override
        public int compareTo(QuarkEvent other) {
            return Long.compare(fNextEventTime, other.getNextEventTime());
        }
    }

    /**
     * Benchmarks creating, single querying and full querying the state system
     */
    @Test
    public void testBenchmark() {
        /* Check arguments */
        long totalTime = this.fNbAvgIntervals * INTERVAL_AVG_TIME;

        Performance perf = Performance.getDefault();
        PerformanceMeter pmBuild = perf.createPerformanceMeter(TEST_PREFIX + TEST_BUILDING_ID + fName);
        perf.tagAsSummary(pmBuild, TEST_BUILDING_ID + fShortName, Dimension.CPU_TIME);

        PerformanceMeter pmSingleQuery = perf.createPerformanceMeter(TEST_PREFIX + TEST_SINGLE_QUERY_ID + fName);
        perf.tagAsSummary(pmSingleQuery, TEST_SINGLE_QUERY_ID + fShortName, Dimension.CPU_TIME);

        PerformanceMeter pmFullQuery = perf.createPerformanceMeter(TEST_PREFIX + TEST_FULL_QUERY_ID + fName);
        perf.tagAsSummary(pmFullQuery, TEST_FULL_QUERY_ID + fShortName, Dimension.CPU_TIME);

        PerformanceMeter pmRangeQuery = perf.createPerformanceMeter(TEST_PREFIX + TEST_QUERY_RANGE_ID + fName);
        perf.tagAsSummary(pmRangeQuery, TEST_QUERY_RANGE_ID + fShortName, Dimension.CPU_TIME);

        PerformanceMeter pm2DQuery = perf.createPerformanceMeter(TEST_PREFIX + TEST_2D_QUERY_ID + fName);
        perf.tagAsSummary(pm2DQuery, TEST_2D_QUERY_ID + fShortName, Dimension.CPU_TIME);

        PerformanceMeter pmReverse2DQuery = perf.createPerformanceMeter(TEST_PREFIX + TEST_REVERSE_2D_QUERY_ID + fName);
        perf.tagAsSummary(pmReverse2DQuery, TEST_REVERSE_2D_QUERY_ID + fShortName, Dimension.CPU_TIME);

        for (int i = 0; i < fNbLoops; i++) {
            try {
                /* Create the state system */
                createFile();
                IStateHistoryBackend backend = StateHistoryBackendFactory.createHistoryTreeBackendNewFile(TEST_BUILDING_ID, NonNullUtils.checkNotNull(fTempFile), 1, 1, QUEUE_SIZE);
                ITmfStateSystemBuilder ss = StateSystemFactory.newStateSystem(backend);

                /* Initialize the attributes */
                Queue<QuarkEvent> quarkEvents = new PriorityQueue<>(fNbAttrib);
                Random randomGenerator = new Random(SEED);
                int rootQuark = ss.getQuarkAbsoluteAndAdd(ROOT_NODE);

                /* Create all attributes before testing */
                for (int j = 0; j < fNbAttrib; j++) {
                    int quark = ss.getQuarkRelativeAndAdd(rootQuark, String.valueOf(j));
                    quarkEvents.add(new QuarkEvent(quark, (Math.abs(randomGenerator.nextLong()) % INTERVAL_AVG_TIME) + 1, fValues.getValues()));
                }

                /* Adds random intervals to the state system */
                pmBuild.start();
                while (true) {
                    QuarkEvent quarkEvent = quarkEvents.poll();
                    if (quarkEvent == null) {
                        break;
                    }
                    long eventTime = quarkEvent.getNextEventTime();
                    ss.modifyAttribute(eventTime, quarkEvent.getNextValue(), quarkEvent.getQuark());
                    long nextDelta = fDistributionMethod.getNextEndTime(randomGenerator, INTERVAL_AVG_TIME);
                    long nextEndTime = eventTime + nextDelta;
                    if (nextEndTime <= totalTime) {
                        quarkEvent.setNextEventTime(nextEndTime);
                        quarkEvents.add(quarkEvent);
                    }
                }
                ss.closeHistory(totalTime);
                pmBuild.stop();

                /*
                 * Benchmark the single queries: for each random timestamp,
                 * query a random attribute
                 */
                List<Integer> subAttributes = ss.getSubAttributes(rootQuark, false);
                pmSingleQuery.start();
                for (int j = 0; j < QUERY_COUNT; j++) {
                    long ts = getNextRandomValue(randomGenerator, totalTime);
                    int attrib = (int) getNextRandomValue(randomGenerator, subAttributes.size());
                    ss.querySingleState(ts, attrib);
                }
                pmSingleQuery.stop();

                /* Benchmark the history range query of 10 attributes */
                pmRangeQuery.start();
                List<Integer> queryAttributes = new ArrayList<>();
                for (int j = 0; j < 10; j++) {
                    int attrib = (int) getNextRandomValue(randomGenerator, subAttributes.size());
                    queryAttributes.add(attrib);
                    StateSystemUtils.queryHistoryRange(ss, attrib, ss.getStartTime(), ss.getCurrentEndTime());
                }
                pmRangeQuery.stop();

                /* Benchmark 2D query of the same 10 attributes */
                pm2DQuery.start();
                for (int j = 0; j < 10; j++) {
                    Iterable<@NonNull ITmfStateInterval> query2d = ss.query2D(queryAttributes, ss.getStartTime(), ss.getCurrentEndTime());
                    Iterator<@NonNull ITmfStateInterval> iterator = query2d.iterator();
                    while (iterator.hasNext()) {
                        iterator.next();
                    }
                }
                pm2DQuery.stop();

                /* Benchmark 2D query of the same 10 attributes, in reverse order */
                pmReverse2DQuery.start();
                for (int j = 0; j < 10; j++) {
                    Iterable<@NonNull ITmfStateInterval> query2d = ss.query2D(queryAttributes, ss.getCurrentEndTime(), ss.getStartTime());
                    Iterator<@NonNull ITmfStateInterval> iterator = query2d.iterator();
                    while (iterator.hasNext()) {
                        iterator.next();
                    }
                }
                pmReverse2DQuery.stop();

                /* Benchmark the full queries */
                pmFullQuery.start();
                for (int j = 0; j < QUERY_COUNT; j++) {
                    long ts = getNextRandomValue(randomGenerator, totalTime);
                    ss.queryFullState(ts);
                }
                pmFullQuery.stop();

                /* Output some data on the file */
                if (i == 0) {
                    if (backend instanceof HistoryTreeBackend) {
                        HistoryTreeBackend htBackend = (HistoryTreeBackend) backend;
                        System.out.println("History tree file size: " + FileUtils.byteCountToDisplaySize(htBackend.getFileSize()));
                        System.out.println("Average node usage: " + htBackend.getAverageNodeUsage());
                    }
                }
                deleteFile();
            } catch (IOException | StateValueTypeException | AttributeNotFoundException | StateSystemDisposedException e) {
                fail(e.getMessage());
            } finally {
                deleteFile();
            }
        }
        pmBuild.commit();
        pmSingleQuery.commit();
        pmFullQuery.commit();
        pmRangeQuery.commit();
        pm2DQuery.commit();
        pmReverse2DQuery.commit();
    }

    /**
     * Get a next random value between 1 and a boundary.
     */
    private static long getNextRandomValue(Random randomGenerator, long limit) {
        long nextLong = Math.abs(randomGenerator.nextLong());
        long nextDelta = (nextLong % limit) + 1;
        return nextDelta;
    }
}
