/*******************************************************************************
 * Copyright (c) 2013, 2016 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
 *
 * Contributors:
 *   Alexandre Montplaisir - Initial API and implementation
 *   Matthew Khouzam - Modified to use a TreeSet
 *   Patrick Tasse - Add message to exceptions
 ******************************************************************************/

package org.eclipse.tracecompass.internal.statesystem.core.backend;

import java.io.File;
import java.io.FileInputStream;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils;
import org.eclipse.tracecompass.internal.provisional.datastore.core.condition.IntegerRangeCondition;
import org.eclipse.tracecompass.internal.provisional.datastore.core.condition.TimeRangeCondition;
import org.eclipse.tracecompass.statesystem.core.backend.IStateHistoryBackend;
import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.statesystem.core.interval.TmfStateInterval;
import com.google.common.collect.Iterables;

/**
 * State history back-end that stores its intervals in RAM only. It cannot be
 * saved to disk, which means we need to rebuild it every time we re-open a
 * trace. But it's relatively quick to build, so this shouldn't be a problem in
 * most cases.
 *
 * This should only be used with very small state histories (and/or, very small
 * traces). Since it's stored in standard Collections, it's limited to 2^31
 * intervals.
 *
 * @author Alexandre Montplaisir
 */
public class InMemoryBackend implements IStateHistoryBackend {

    private static final @NonNull Logger LOGGER = TraceCompassLog.getLogger(InMemoryBackend.class);

    private final @NonNull String ssid;
    private final NavigableSet<@NonNull ITmfStateInterval> intervals;
    private final long startTime;

    private volatile long latestTime;

    /**
     * Constructor
     *
     * @param ssid
     *            The state system's ID
     * @param startTime
     *            The start time of this interval store
     */
    public InMemoryBackend(@NonNull String ssid, long startTime) {
        this.ssid = ssid;
        this.startTime = startTime;
        this.latestTime = startTime;
        /**
         * We need to compare the end time and the attribute, because we can
         * have 2 intervals with the same end time (for different attributes).
         * And TreeSet needs a unique "key" per element.
         */
        this.intervals = new TreeSet<>(Comparator
                .comparing(ITmfStateInterval::getEndTime)
                .thenComparing(ITmfStateInterval::getAttribute));
    }

    @Override
    public String getSSID() {
        return ssid;
    }

    @Override
    public long getStartTime() {
        return startTime;
    }

    @Override
    public long getEndTime() {
        return latestTime;
    }

    @Override
    public void insertPastState(long stateStartTime, long stateEndTime,
            int quark, Object value) throws TimeRangeException {
        /* Make sure the passed start/end times make sense */
        if (stateStartTime > stateEndTime || stateStartTime < startTime) {
            throw new TimeRangeException(ssid + " Interval Start:" + stateStartTime + ", Interval End:" + stateEndTime + ", Backend Start:" + startTime); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        }

        ITmfStateInterval interval = new TmfStateInterval(stateStartTime, stateEndTime, quark, value);

        /* Add the interval into the tree */
        synchronized (intervals) {
            intervals.add(interval);
        }

        /* Update the "latest seen time" */
        if (stateEndTime > latestTime) {
            latestTime = stateEndTime;
        }
    }

    @Override
    public void doQuery(List<ITmfStateInterval> currentStateInfo, long t)
            throws TimeRangeException {
        if (!checkValidTime(t)) {
            throw new TimeRangeException(ssid + " Time:" + t + ", Start:" + startTime + ", End:" + latestTime); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        }

        /*
         * The intervals are sorted by end time, so we can binary search to get
         * the first possible interval, then only compare their start times.
         */
        synchronized (intervals) {
            Iterator<ITmfStateInterval> iter = searchforEndTime(intervals, 0, t).iterator();
            for (int modCount = 0; iter.hasNext() && modCount < currentStateInfo.size();) {
                ITmfStateInterval entry = iter.next();
                final long entryStartTime = entry.getStartTime();
                if (entryStartTime <= t) {
                    /* Add this interval to the returned values */
                    currentStateInfo.set(entry.getAttribute(), entry);
                    modCount++;
                }
            }
        }
    }

    @Override
    public ITmfStateInterval doSingularQuery(long t, int attributeQuark)
            throws TimeRangeException {
        if (!checkValidTime(t)) {
            throw new TimeRangeException(ssid + " Time:" + t + ", Start:" + startTime + ", End:" + latestTime); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        }

        /*
         * The intervals are sorted by end time, so we can binary search to get
         * the first possible interval, then only compare their start times.
         */
        synchronized (intervals) {
            Iterable<ITmfStateInterval> iter = searchforEndTime(intervals, attributeQuark, t);
            for (ITmfStateInterval entry : iter) {
                final boolean attributeMatches = (entry.getAttribute() == attributeQuark);
                final long entryStartTime = entry.getStartTime();
                if (attributeMatches) {
                    if (entryStartTime <= t) {
                        /* This is the droid we are looking for */
                        return entry;
                    }
                }
            }
        }
        return null;
    }

    private boolean checkValidTime(long t) {
        return (t >= startTime && t <= latestTime);
    }

    @Override
    public void finishedBuilding(long endTime) throws TimeRangeException {
        /* Nothing to do */
    }

    @Override
    public FileInputStream supplyAttributeTreeReader() {
        /* Saving to disk not supported */
        return null;
    }

    @Override
    public File supplyAttributeTreeWriterFile() {
        /* Saving to disk not supported */
        return null;
    }

    @Override
    public long supplyAttributeTreeWriterFilePosition() {
        /* Saving to disk not supported */
        return -1;
    }

    @Override
    public void removeFiles() {
        /* Nothing to do */
    }

    @Override
    public void dispose() {
        /* Nothing to do */
    }

    private static Iterable<@NonNull ITmfStateInterval> searchforEndTime(NavigableSet<@NonNull ITmfStateInterval> tree, int quark, long time) {
        ITmfStateInterval dummyInterval = new TmfStateInterval(-1, time, quark, (Object) null);
        return tree.tailSet(dummyInterval);
    }

    @Override
    public Iterable<@NonNull ITmfStateInterval> query2D(IntegerRangeCondition quarks, TimeRangeCondition times)
            throws TimeRangeException {
        try (TraceCompassLogUtils.ScopeLog log = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "InMemoryBackend:query2D", //$NON-NLS-1$
                "ssid", getSSID(), //$NON-NLS-1$
                "quarks", quarks, //$NON-NLS-1$
                "times", times)) { //$NON-NLS-1$
            synchronized (intervals) {
                return Iterables.filter(searchforEndTime(intervals, quarks.min(), times.min()),
                        interval -> quarks.test(interval.getAttribute())
                                && times.intersects(interval.getStartTime(), interval.getEndTime()));
            }
        }
    }

}
