/*******************************************************************************
 * Copyright (c) 2012, 2014 Ericsson, É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
 *
 * Contributors:
 *   Matthew Khouzam - Initial API and implementation
 *   Florian Wininger - Performance improvements
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.ctf.core.trace.iterator;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.Objects;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.ctf.core.CTFException;
import org.eclipse.tracecompass.ctf.core.event.IEventDefinition;
import org.eclipse.tracecompass.ctf.core.trace.CTFStreamInputReader;
import org.eclipse.tracecompass.ctf.core.trace.CTFTrace;
import org.eclipse.tracecompass.ctf.core.trace.CTFTraceReader;
import org.eclipse.tracecompass.internal.tmf.ctf.core.Activator;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.trace.ITmfContext;
import org.eclipse.tracecompass.tmf.core.trace.location.ITmfLocation;
import org.eclipse.tracecompass.tmf.ctf.core.context.CtfLocation;
import org.eclipse.tracecompass.tmf.ctf.core.context.CtfLocationInfo;
import org.eclipse.tracecompass.tmf.ctf.core.event.CtfTmfEvent;
import org.eclipse.tracecompass.tmf.ctf.core.trace.CtfTmfTrace;

/**
 * The CTF trace reader iterator.
 *
 * It doesn't reserve a file handle, so many iterators can be used without
 * worries of I/O errors or resource exhaustion.
 *
 * @author Matthew Khouzam
 */
public class CtfIterator extends CTFTraceReader
        implements ITmfContext, Comparable<CtfIterator> {

    /** An invalid location */
    public static final CtfLocation NULL_LOCATION = new CtfLocation(CtfLocation.INVALID_LOCATION);

    private final @NonNull CtfTmfTrace fTrace;

    private CtfLocation fCurLocation;
    private long fCurRank;

    private CtfLocation fPreviousLocation;
    private CtfTmfEvent fPreviousEvent;

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------

    /**
     * Create a new CTF trace iterator, which initially points at the first
     * event in the trace.
     *
     * @param ctfTrace
     *            The {@link CTFTrace} linked to the trace. It should be
     *            provided by the corresponding 'ctfTmfTrace'.
     *
     * @param ctfTmfTrace
     *            The {@link CtfTmfTrace} to iterate over
     * @throws CTFException
     *             If the iterator couldn't not be instantiated, probably due to
     *             a read error.
     */
    public CtfIterator(CTFTrace ctfTrace, @NonNull CtfTmfTrace ctfTmfTrace) throws CTFException {
        super(ctfTrace);
        fTrace = ctfTmfTrace;
        if (hasMoreEvents()) {
            fCurLocation = new CtfLocation(ctfTmfTrace.getStartTime());
            fCurRank = 0;
        } else {
            setUnknownLocation();
        }
    }

    /**
     * Create a new CTF trace iterator, which will initially point to the given
     * location/rank.
     *
     * @param ctfTrace
     *            The {@link CTFTrace} linked to the trace. It should be
     *            provided by the corresponding 'ctfTmfTrace'.
     * @param ctfTmfTrace
     *            The {@link CtfTmfTrace} to iterate over
     * @param ctfLocationData
     *            The initial timestamp the iterator will be pointing to
     * @param rank
     *            The initial rank
     * @throws CTFException
     *             If the iterator couldn't not be instantiated, probably due to
     *             a read error.
     */
    public CtfIterator(CTFTrace ctfTrace, @NonNull CtfTmfTrace ctfTmfTrace, CtfLocationInfo ctfLocationData, long rank)
            throws CTFException {
        super(ctfTrace);

        this.fTrace = ctfTmfTrace;
        if (this.hasMoreEvents()) {
            this.fCurLocation = new CtfLocation(ctfLocationData);
            if (this.getCurrentEvent().getTimestamp().getValue() != ctfLocationData.getTimestamp()) {
                this.seek(ctfLocationData);
                this.fCurRank = rank;
            }
        } else {
            setUnknownLocation();
        }
    }

    @Override
    public void dispose() {
        close();
    }

    private void setUnknownLocation() {
        fCurLocation = NULL_LOCATION;
        fCurRank = UNKNOWN_RANK;
    }

    // ------------------------------------------------------------------------
    // Accessors
    // ------------------------------------------------------------------------

    /**
     * Return this iterator's trace.
     *
     * @return CtfTmfTrace The iterator's trace
     */
    public CtfTmfTrace getCtfTmfTrace() {
        return fTrace;
    }

    /**
     * Return the current event pointed to by the iterator.
     *
     * @return CtfTmfEvent The current event
     */
    public synchronized CtfTmfEvent getCurrentEvent() {
        final CTFStreamInputReader top = super.getPrio().peek();
        if (top != null) {
            if (!fCurLocation.equals(fPreviousLocation)) {
                fPreviousLocation = fCurLocation;
                fPreviousEvent = fTrace.getEventFactory().createEvent(fTrace, checkNotNull(top.getCurrentEvent()), top.getFilename());
            }
            return fPreviousEvent;
        }
        return null;
    }

    /**
     * Return the current timestamp location pointed to by the iterator. This is
     * the timestamp for use in CtfLocation, not the event timestamp.
     *
     * @return long The current timestamp location
     */
    public synchronized long getCurrentTimestamp() {
        final CTFStreamInputReader top = super.getPrio().peek();
        if (top != null) {
            IEventDefinition currentEvent = top.getCurrentEvent();
            if (currentEvent != null) {
                long ts = currentEvent.getTimestamp();
                return fTrace.timestampCyclesToNanos(ts);
            }
        }
        return 0;
    }

    /**
     * Seek this iterator to a given location.
     *
     * @param ctfLocationData
     *            The LocationData representing the position to seek to
     * @return boolean True if the seek was successful, false if there was an
     *         error seeking.
     */
    public synchronized boolean seek(CtfLocationInfo ctfLocationData) {
        boolean ret = false;
        if (ctfLocationData.equals(CtfLocation.INVALID_LOCATION)) {
            fCurLocation = NULL_LOCATION;
            return false;
        }

        /* Avoid the cost of seeking at the current location. */
        if (fCurLocation.getLocationInfo().equals(ctfLocationData)) {
            return super.hasMoreEvents();
        }
        /* Update location to make sure the current event is updated */
        fCurLocation = new CtfLocation(ctfLocationData);

        /* Adjust the timestamp depending on the trace's offset */
        final long seekToTimestamp = ctfLocationData.getTimestamp();
        final long offsetTimestamp = this.getCtfTmfTrace().timestampNanoToCycles(seekToTimestamp);
        try {
            if (offsetTimestamp < 0) {
                ret = super.seek(0L);
            } else {
                ret = super.seek(offsetTimestamp);
            }
        } catch (CTFException e) {
            Activator.getDefault().logError(e.getMessage(), e);
            return false;
        }
        /*
         * Check if there is already one or more events for that timestamp, and
         * assign the location index correctly
         */
        long index = 0;
        ITmfEvent currentEvent = getCurrentEvent();
        ret &= (currentEvent != null);
        long offset = ctfLocationData.getIndex();
        while (currentEvent != null && index < offset) {
            if (seekToTimestamp >= Objects.requireNonNull(currentEvent).getTimestamp().getValue()) {
                index++;
            } else {
                index = 0;
                break;
            }
            ret = advance();
            currentEvent = getCurrentEvent();
        }
        /* Update the current location accordingly */
        if (ret) {
            long time = Objects.requireNonNull(currentEvent).getTimestamp().getValue();
            fCurLocation = new CtfLocation(new CtfLocationInfo(time, time != seekToTimestamp ? 0 : index));
        } else {
            fCurLocation = NULL_LOCATION;
        }
        return ret;
    }

    // ------------------------------------------------------------------------
    // CTFTraceReader
    // ------------------------------------------------------------------------

    @Override
    public boolean seek(long timestamp) {
        return seek(new CtfLocationInfo(timestamp, 0));
    }

    @Override
    public synchronized boolean advance() {
        boolean ret = false;
        try {
            ret = super.advance();
        } catch (CTFException e) {
            Activator.getDefault().logError(e.getMessage(), e);
        }

        if (ret) {
            long timestamp = fCurLocation.getLocationInfo().getTimestamp();
            final long timestampValue = getCurrentTimestamp();
            if (timestamp == timestampValue) {
                long index = fCurLocation.getLocationInfo().getIndex();
                fCurLocation = new CtfLocation(timestampValue, index + 1);
            } else {
                fCurLocation = new CtfLocation(timestampValue, 0L);
            }
        } else {
            fCurLocation = NULL_LOCATION;
        }
        return ret;
    }

    // ------------------------------------------------------------------------
    // ITmfContext
    // ------------------------------------------------------------------------

    @Override
    public long getRank() {
        return fCurRank;
    }

    @Override
    public void setRank(long rank) {
        fCurRank = rank;
    }

    @Override
    public void increaseRank() {
        /* Only increase the rank if it's valid */
        if (hasValidRank()) {
            fCurRank++;
        }
    }

    @Override
    public boolean hasValidRank() {
        return (getRank() >= 0);
    }

    @Override
    public void setLocation(ITmfLocation location) {
        // FIXME alex: isn't there a cleaner way than a cast here?
        fCurLocation = (CtfLocation) location;
        seek(((CtfLocation) location).getLocationInfo());
    }

    @Override
    public CtfLocation getLocation() {
        return fCurLocation;
    }

    // ------------------------------------------------------------------------
    // Comparable
    // ------------------------------------------------------------------------

    @Override
    public int compareTo(final CtfIterator o) {
        if (getRank() < o.getRank()) {
            return -1;
        } else if (getRank() > o.getRank()) {
            return 1;
        }
        return 0;
    }

    // ------------------------------------------------------------------------
    // Object
    // ------------------------------------------------------------------------

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), fTrace, fCurLocation, fCurRank);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!super.equals(obj)) {
            return false;
        }
        if (!(obj instanceof CtfIterator)) {
            return false;
        }
        CtfIterator other = (CtfIterator) obj;
        if (!Objects.equals(fTrace, other.fTrace)) {
            return false;
        }
        if (!Objects.equals(fCurLocation, other.fCurLocation)) {
            return false;
        }
        return (fCurRank == other.fCurRank);
    }
}
