/*******************************************************************************
 * Copyright (c) 2011, 2014 Ericsson, Ecole Polytechnique de Montreal and others
 *
 * 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
 * Contributors: Simon Marchi - Initial API and implementation
 *******************************************************************************/

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.ctf.core.CTFException;
import org.eclipse.tracecompass.ctf.core.event.IEventDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.IDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.IEventHeaderDeclaration;
import org.eclipse.tracecompass.ctf.core.event.types.StructDeclaration;
import org.eclipse.tracecompass.ctf.core.trace.CTFStreamInput;
import org.eclipse.tracecompass.ctf.core.trace.CTFTrace;
import org.eclipse.tracecompass.ctf.core.trace.ICTFStream;
import org.eclipse.tracecompass.internal.ctf.core.event.EventDeclaration;
import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException;
import org.eclipse.tracecompass.internal.ctf.core.utils.SparseList;

/**
 * <b><u>Stream</u></b>
 * <p>
 * Represents a stream in a trace.
 */
public class CTFStream implements ICTFStream {

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------

    /**
     * The numerical ID of the stream
     */
    private long fId = 0;

    /**
     * Declarations of the stream-specific structures
     */
    private StructDeclaration fPacketContextDecl = null;
    private IDeclaration fEventHeaderDecl = null;
    private StructDeclaration fEventContextDecl = null;

    /**
     * The trace to which the stream belongs
     */
    private CTFTrace fTrace = null;

    /**
     * Maps event ids to events
     */
    private List<@Nullable IEventDeclaration> fEvents = new ArrayList<>();

    private boolean fEventUnsetId = false;
    private boolean fStreamIdSet = false;

    /**
     * The inputs associated to this stream
     */
    private final Set<CTFStreamInput> fInputs = new HashSet<>();

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

    /**
     * Constructs a Stream that belongs to a Trace
     *
     * @param trace
     *            The trace to which belongs this stream.
     */
    public CTFStream(CTFTrace trace) {
        fTrace = trace;
    }

    // ------------------------------------------------------------------------
    // Getters/Setters/Predicates
    // ------------------------------------------------------------------------

    /**
     * Sets the id of a stream
     *
     * @param id
     *            the id of a stream
     */
    public void setId(long id) {
        fId = id;
        fStreamIdSet = true;
    }

    @Override
    public long getId() {
        return fId;
    }

    @Override
    public boolean isIdSet() {
        return fStreamIdSet;
    }

    @Override
    public boolean isEventHeaderSet() {
        return fEventHeaderDecl != null;
    }

    @Override
    public boolean isEventContextSet() {
        return fEventContextDecl != null;
    }

    @Override
    public boolean isPacketContextSet() {
        return fPacketContextDecl != null;
    }

    /**
     * Sets the event header
     *
     * @param eventHeader
     *            the current event header for all events in this stream
     */
    public void setEventHeader(StructDeclaration eventHeader) {
        fEventHeaderDecl = eventHeader;
    }

    /**
     * Sets the event header, this typically has the id and the timestamp
     *
     * @param eventHeader
     *            the current event header for all events in this stream
     */
    public void setEventHeader(IEventHeaderDeclaration eventHeader) {
        fEventHeaderDecl = eventHeader;
    }

    /**
     *
     * @param eventContext
     *            the context for all events in this stream
     */
    public void setEventContext(StructDeclaration eventContext) {
        fEventContextDecl = eventContext;
    }

    /**
     *
     * @param packetContext
     *            the packet context for all packets in this stream
     */
    public void setPacketContext(StructDeclaration packetContext) {
        fPacketContextDecl = packetContext;
    }

    @Override
    public IDeclaration getEventHeaderDeclaration() {
        return fEventHeaderDecl;
    }

    @Override
    public StructDeclaration getEventContextDecl() {
        return fEventContextDecl;
    }

    @Override
    public StructDeclaration getPacketContextDecl() {
        return fPacketContextDecl;
    }

    @Override
    public Set<CTFStreamInput> getStreamInputs() {
        return fInputs;
    }

    @Override
    public CTFTrace getTrace() {
        return fTrace;
    }

    /**
     * @since 2.0
     */
    @Override
    public List<@Nullable IEventDeclaration> getEventDeclarations() {
        return Collections.unmodifiableList(fEvents);
    }

    @Override
    public IEventDeclaration getEventDeclaration(int eventId) {
        int eventIndex = (eventId == IEventDeclaration.UNSET_EVENT_ID) ? 0 : eventId;
        if (eventIndex < 0) {
            /* Any negative value other than UNSET_EVENT_ID is invalid */
            throw new IllegalArgumentException("Event ID cannot be negative."); //$NON-NLS-1$
        }
        if (eventIndex >= fEvents.size()) {
            /* This ID could be valid, but there are no declarations with it */
            return null;
        }
        return fEvents.get(eventIndex);
    }

    // ------------------------------------------------------------------------
    // Operations
    // ------------------------------------------------------------------------

    /**
     * Adds an event to the event list.
     *
     * An event in a stream can omit its id if it is the only event in this
     * stream. An event for which no id has been specified has a null id. It is
     * thus not possible to add an event with the null key if the map is not
     * empty. It is also not possible to add an event to the map if the null key
     * is present in the map.
     *
     * @param event
     *            The event to add
     * @throws ParseException
     *             If there was a problem reading the event or adding it to the
     *             stream
     * @since 2.0
     */
    public void addEvent(IEventDeclaration event) throws ParseException {
        if (fEventUnsetId) {
            throw new ParseException("Event without id with multiple events in a stream"); //$NON-NLS-1$
        }
        int id = ((EventDeclaration) event).id();

        /*
         * If there is an event without id (the null key), it must be the only
         * one
         */
        if (id == IEventDeclaration.UNSET_EVENT_ID) {
            if (!fEvents.isEmpty()) {
                throw new ParseException("Event without id with multiple events in a stream"); //$NON-NLS-1$
            }
            fEventUnsetId = true;
            fEvents.add(event);
        } else {
            /* Check if an event with the same ID already exists */
            if (fEvents.size() > id && fEvents.get(id) != null) {
                throw new ParseException("Event id already exists"); //$NON-NLS-1$
            }
            ensureSize(id);
            /* Put the event in the list */
            fEvents.set(id, event);
        }
    }

    /**
     * Add a list of event declarations to this stream. There must be no overlap
     * between the two lists of event declarations. This will merge the two
     * lists and preserve the indexes of both lists.
     *
     * @param events
     *            list of the events to add
     * @throws CTFException
     *             if the list already contains data
     */
    public void addEvents(Collection<IEventDeclaration> events) throws CTFException {
        if (fEventUnsetId) {
            throw new CTFException("Cannot add to a stream with an unidentified event"); //$NON-NLS-1$
        }
        if (fEvents.isEmpty()) {
            fEvents.addAll(events);
            return;
        }
        for (IEventDeclaration event : events) {
            if (event != null) {
                int index = event.getId().intValue();
                ensureSize(index);
                if (fEvents.get(index) != null) {
                    throw new CTFException("Both lists have an event defined at position " + index); //$NON-NLS-1$
                }
                fEvents.set(index, event);
            }
        }
    }

    private void ensureSize(int index) {
        List<@Nullable IEventDeclaration> list = fEvents;
        if (list instanceof ArrayList) {
            if (index > 50000) {
                SparseList<@Nullable IEventDeclaration> sparseList = new SparseList<>(fEvents);
                sparseList.ensureSize(index + 1);
                fEvents = sparseList;
                return;
            }
            ((ArrayList<@Nullable IEventDeclaration>) list).ensureCapacity(index);
            while (list.size() <= index) {
                list.add(null);
            }
        } else if (list instanceof SparseList) {
            SparseList sparseList = (SparseList) list;
            sparseList.ensureSize(index + 1);
        }
    }

    /**
     * Add an input to this Stream
     *
     * @param input
     *            The StreamInput to add.
     */
    public void addInput(CTFStreamInput input) {
        fInputs.add(input);
    }

    @Override
    public String toString() {
        return "Stream [id=" + fId + ", packetContextDecl=" + fPacketContextDecl //$NON-NLS-1$ //$NON-NLS-2$
                + ", eventHeaderDecl=" + fEventHeaderDecl //$NON-NLS-1$
                + ", eventContextDecl=" + fEventContextDecl + ", trace=" + fTrace //$NON-NLS-1$ //$NON-NLS-2$
                + ", events=" + fEvents + ", inputs=" + fInputs + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    }
}
