/*******************************************************************************
 * Copyright (c) 2011, 2015 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:
 *   Marc Dumais - Initial implementation
 *   Francois Chouinard - Initial API and implementation
 *   Patrick Tasse - Updated for TMF 2.0
 *   Matthew Khouzam - update validate
 *******************************************************************************/

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

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;

import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.gdb.IGdbDebugPreferenceConstants;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tracecompass.internal.gdbtrace.core.GdbTraceCorePlugin;
import org.eclipse.tracecompass.internal.gdbtrace.core.event.GdbTraceEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.aspect.ITmfEventAspect;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfTraceException;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.trace.ITmfContext;
import org.eclipse.tracecompass.tmf.core.trace.TmfContext;
import org.eclipse.tracecompass.tmf.core.trace.TmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TraceValidationStatus;
import org.eclipse.tracecompass.tmf.core.trace.location.ITmfLocation;
import org.eclipse.tracecompass.tmf.core.trace.location.TmfLongLocation;

/**
 * GDB Tracepoint extension of TmfTrace. This class implements the necessary
 * methods and functionalities so that a GDB tracepoint file can be used by the
 * TMF framework as a "tracer".
 * <p>
 *
 * @author Marc Dumais
 * @author Francois Chouinard
 * @author Matthew Khouzam
 */
@SuppressWarnings("restriction")
public class GdbTrace extends TmfTrace {

    // ------------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------------

    private static final int CACHE_SIZE = 20;

    private static final byte[] HEADER = new byte[] {0x7f, 'T', 'R', 'A', 'C', 'E'};

    /** The qualified name for the 'executable' persistent property */
    public static final QualifiedName EXEC_KEY = new QualifiedName(GdbTraceCorePlugin.PLUGIN_ID, "executable"); //$NON-NLS-1$

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

    // Interface to access GDB Tracepoints
    private DsfGdbAdaptor fGdbTpRef;
    private long fNbFrames = 0;

    // The trace location
    long fLocation;

    // ------------------------------------------------------------------------
    // Constructor
    // ------------------------------------------------------------------------

    /**
     * Default constructor
     */
    public GdbTrace() {
        setCacheSize(CACHE_SIZE);
    }

    @Override
    public IStatus validate(IProject project, String path) {
        File file = new File(path);
        if (!file.exists()) {
            return new Status(IStatus.ERROR, GdbTraceCorePlugin.PLUGIN_ID,
                    NLS.bind(Messages.GdbTrace_FileNotFound, path));
        }
        if (!file.isFile()) {
            return new Status(IStatus.ERROR, GdbTraceCorePlugin.PLUGIN_ID,
                    NLS.bind(Messages.GdbTrace_GdbTracesMustBeAFile, path));
        }
        try (BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file))) {
            byte[] buffer = new byte[HEADER.length];
            int read = stream.read(buffer);
            if (read != HEADER.length || !Arrays.equals(buffer, HEADER)) {
                return new Status(IStatus.ERROR, GdbTraceCorePlugin.PLUGIN_ID,
                        NLS.bind(Messages.GdbTrace_NotGdbTraceFile, path));
            }
        } catch (IOException e) {
            return new Status(IStatus.ERROR, GdbTraceCorePlugin.PLUGIN_ID,
                    NLS.bind(Messages.GdbTrace_IOException, path), e);
        }
        return new TraceValidationStatus(100, GdbTraceCorePlugin.PLUGIN_ID);
    }

    @Override
    public void initTrace(IResource resource, String path, Class<? extends ITmfEvent> type) throws TmfTraceException {
        try {
            String tracedExecutable = resource.getPersistentProperty(EXEC_KEY);
            if (tracedExecutable == null) {
                throw new TmfTraceException(Messages.GdbTrace_ExecutableNotSet);
            }

            String defaultGdbCommand = Platform.getPreferencesService().getString(GdbPlugin.PLUGIN_ID,
                    IGdbDebugPreferenceConstants.PREF_DEFAULT_GDB_COMMAND,
                    IGDBLaunchConfigurationConstants.DEBUGGER_DEBUG_NAME_DEFAULT, null);

            fGdbTpRef = new DsfGdbAdaptor(this, defaultGdbCommand, path, tracedExecutable);
            fNbFrames = getNbFrames();
        } catch (CoreException e) {
            throw new TmfTraceException(Messages.GdbTrace_FailedToInitializeTrace, e);
        }

        super.initTrace(resource, path, type);
    }

    @Override
    public synchronized void dispose() {
        if (fGdbTpRef != null) {
            fGdbTpRef.dispose();
        }
        super.dispose();
    }

    /**
     * @return GDB-DSF session id
     */
    public String getDsfSessionId() {
        return fGdbTpRef.getSessionId();
    }

    /**
     * @return the number of frames in current tp session
     */
    public synchronized long getNbFrames() {
        fNbFrames = fGdbTpRef.getNumberOfFrames();
        return fNbFrames;
    }

    // ------------------------------------------------------------------------
    // TmfTrace
    // ------------------------------------------------------------------------

    @Override
    public Iterable<ITmfEventAspect<?>> getEventAspects() {
        return GdbEventAspects.getAspects();
    }

    @Override
    public synchronized TmfContext seekEvent(ITmfLocation location) {
        fLocation = (location != null) ? ((Long) location.getLocationInfo()) : 0;
        return new TmfContext(new TmfLongLocation(fLocation), fLocation);
    }

    @Override
    public synchronized ITmfContext seekEvent(double ratio) {
        TmfContext context = seekEvent((long) ratio * getNbEvents());
        return context;
    }

    @Override
    public double getLocationRatio(ITmfLocation location) {
        if (getNbEvents() > 0 && location instanceof TmfLongLocation) {
            return (double) ((TmfLongLocation) location).getLocationInfo() / getNbEvents();
        }
        return 0;
    }

    @Override
    public ITmfLocation getCurrentLocation() {
        return new TmfLongLocation(fLocation);
    }

    @Override
    public GdbTraceEvent parseEvent(ITmfContext context) {
        if (context.getRank() >= fNbFrames) {
            return null;
        }
        // work-around to ensure that the select and parse of trace frame will
        // be atomic
        GdbTraceEvent event = fGdbTpRef.selectAndReadFrame(context.getRank());
        fLocation++;
        return event;
    }

    @Override
    public synchronized TmfContext seekEvent(ITmfTimestamp timestamp) {
        long rank = timestamp.getValue();
        return seekEvent(rank);
    }

    @Override
    public synchronized TmfContext seekEvent(long rank) {
        fLocation = rank;
        TmfContext context = new TmfContext(new TmfLongLocation(fLocation), rank);
        return context;
    }

    /**
     * Select a frame and update the visualization
     *
     * @param rank
     *            the rank
     */
    public synchronized void selectFrame(long rank) {
        fGdbTpRef.selectDataFrame(rank, true);
    }
}