/*******************************************************************************
 * Copyright (c) 2014, 2015 É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:
 *   Geneviève Bastien - Initial implementation
 *   Patrick Tasse - Dispose wrapped trace
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.tests.stubs.trace.xml;

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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.stream.StreamSupport;

import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.annotation.DefaultLocation;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.internal.util.ByteBufferTracker;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventType;
import org.eclipse.tracecompass.tmf.core.event.TmfEvent;
import org.eclipse.tracecompass.tmf.core.event.TmfEventField;
import org.eclipse.tracecompass.tmf.core.event.TmfEventType;
import org.eclipse.tracecompass.tmf.core.event.aspect.ITmfEventAspect;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfBaseAspects;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfContentFieldAspect;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfCpuAspect;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfTraceException;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomEventContent;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlEvent;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlTrace;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlTraceDefinition;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
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.indexer.ITmfTraceIndexer;
import org.eclipse.tracecompass.tmf.core.trace.indexer.checkpoint.TmfCheckpointIndexer;
import org.eclipse.tracecompass.tmf.core.trace.location.ITmfLocation;
import org.xml.sax.SAXException;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

/**
 * An XML development trace using a custom XML trace definition and schema.
 *
 * This class will typically be used to build custom traces to unit test more
 * complex functionalities like analyzes or to develop and test data-driven
 * analyzes.
 *
 * This class wraps a custom XML trace and rewrites the returned events in the
 * getNext() method so that event's fields are the ones defined in <field ... />
 * elements instead of those defined in the custom XML parser. This way, each
 * event can have a different set of fields. This class can, for example, mimic
 * a CTF trace.
 *
 * @author Geneviève Bastien
 */
@SuppressWarnings("restriction")
public abstract class TmfXmlTraceStub extends TmfTrace {

    private static final String DEVELOPMENT_TRACE_XSD = "TmfXmlDevelopmentTrace.xsd"; //$NON-NLS-1$
    private static final String EMPTY = ""; //$NON-NLS-1$

    /* XML elements and attributes names */
    private static final String EVENT_NAME_FIELD = "Message"; //$NON-NLS-1$
    private static final String FIELD_NAMES_FIELD = "fields"; //$NON-NLS-1$
    private static final String VALUES_FIELD = "values"; //$NON-NLS-1$
    private static final String TYPES_FIELD = "type"; //$NON-NLS-1$
    private static final String VALUES_SEPARATOR = " \\| "; //$NON-NLS-1$
    private static final String TYPE_INTEGER = "int"; //$NON-NLS-1$
    private static final String TYPE_LONG = "long"; //$NON-NLS-1$
    private static final String TYPE_LONG_ARRAY = "longArray"; //$NON-NLS-1$
    private static final String TYPE_INT_ARRAY = "intArray"; //$NON-NLS-1$
    private static final String ASPECT_CPU = "cpu";

    private final CustomXmlTraceDefinition fDefinition;
    private CustomXmlTrace fTrace;

    private Collection<ITmfEventAspect<?>> fAspects = TmfTrace.BASE_ASPECTS;
    private final Collection<ITmfEventAspect<?>> fAdditionalAspects = new HashSet<>();
    private final Collection<IAnalysisModule> fAdditionalModules = new HashSet<>();

    /**
     * Constructor. Constructs the custom XML trace with the appropriate
     * definition.
     */
    public TmfXmlTraceStub() {
        boolean nonNullTraceIsNonNull = false;
        /* Load custom XML definition */
        try (InputStream in = TmfXmlTraceStub.class.getResourceAsStream(getParserFileName());) {
            CustomXmlTraceDefinition[] definitions = CustomXmlTraceDefinition.loadAll(in);
            if (definitions.length < 2) {
                throw new IllegalStateException("The custom trace definition does not exist"); //$NON-NLS-1$
            }
            /* The first definition parses the 'set_aspects' event */
            fTrace = new CustomXmlTrace(definitions[0]) {
                @Override
                protected ITmfTraceIndexer createIndexer(int interval) {
                    /* Use the in-memory checkpoint indexer */
                    return new TmfCheckpointIndexer(this, interval);
                }
            };
            TmfSignalManager.deregister(fTrace);
            nonNullTraceIsNonNull = true;
            /* The second definition parses 'event' trace events */
            fDefinition = checkNotNull(definitions[1]);
        } catch (IOException e) {
            if (nonNullTraceIsNonNull) {
                fTrace.dispose();
            }
            throw new IllegalStateException("Cannot open the trace parser for development traces"); //$NON-NLS-1$
        }

    }

    /**
     * Get the name of the file that contains the XML parser for this trace
     * stub. The file should be located in this class's package.
     *
     * @return The name of the file
     */
    protected abstract String getParserFileName();

    @Override
    @NonNullByDefault({DefaultLocation.TYPE_ARGUMENT})
    public void initTrace(@Nullable IResource resource, @Nullable String path, @Nullable Class<? extends ITmfEvent> type) throws TmfTraceException {
        super.initTrace(resource, path, type);
        ITmfContext ctx;

        /* Initialize and read the trace with the 'set_aspects' definition */
        TmfSignalManager.deregister(fTrace);
        fTrace.initTrace(resource, path, type);
        ctx = seekEvent(0L);
        /* If a set_aspects event exists, getNext() will process it */
        getNext(ctx);
        ctx.dispose();
        fTrace.dispose();

        /* Initialize a new trace with the trace events definition */
        fTrace = new CustomXmlTrace(fDefinition);
        TmfSignalManager.deregister(fTrace);
        fTrace.initTrace(resource, path, type);
        /* Set the start and (current) end times for this trace */
        ctx = seekEvent(0L);
        if (ctx == null) {
            return;
        }
        ITmfEvent event = getNext(ctx);
        if (event != null) {
            final ITmfTimestamp curTime = event.getTimestamp();
            this.setStartTime(curTime);
            this.setEndTime(curTime);
        }
        ctx.dispose();
    }

    @Override
    public synchronized void dispose() {
        super.dispose();
        fTrace.dispose();
    }

    @Override
    public @Nullable ITmfEvent parseEvent(@Nullable ITmfContext context) {
        return fTrace.parseEvent(context);
    }

    @Override
    public @Nullable ITmfLocation getCurrentLocation() {
        return fTrace.getCurrentLocation();
    }

    @Override
    public double getLocationRatio(@Nullable ITmfLocation location) {
        return fTrace.getLocationRatio(location);
    }

    @Override
    public @Nullable ITmfContext seekEvent(@Nullable ITmfLocation location) {
        return fTrace.seekEvent(location);
    }

    @Override
    public @Nullable ITmfContext seekEvent(double ratio) {
        return fTrace.seekEvent(ratio);
    }

    @Override
    public IStatus validate(@Nullable IProject project, @Nullable String path) {
        File xmlFile = new File(path);
        if (!xmlFile.exists() || !xmlFile.isFile() || !xmlFile.canRead()) {
            return new Status(IStatus.ERROR, Activator.PLUGIN_ID, NLS.bind(org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.Messages.TmfDevelopmentTrace_FileNotFound, path));
        }
        /* Does the XML file validate with the XSD */
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Source xmlSource = new StreamSource(xmlFile);

        try {
            URL url = TmfXmlTraceStub.class.getResource(DEVELOPMENT_TRACE_XSD);
            Schema schema = schemaFactory.newSchema(url);

            Validator validator = schema.newValidator();
            validator.validate(xmlSource);
        } catch (SAXException e) {
            return new Status(IStatus.ERROR, Activator.PLUGIN_ID, NLS.bind(org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.Messages.TmfDevelopmentTrace_ValidationError, path), e);
        } catch (IOException e) {
            return new Status(IStatus.ERROR, Activator.PLUGIN_ID, NLS.bind(org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.Messages.TmfDevelopmentTrace_IoError, path), e);
        } finally {
            /**
             * The Validator keeps a reference to the file resource when it fails. In Windows, this reference will
             * prevent the deletion of the file. Running a GC will remove the reference. Use the ByteBufferTracker
             * to trigger a GC before the deletion of a trace element.
             */
            ByteBufferTracker.setMarked();
        }
        // XML validates, we have confidence it is this trace type
        IStatus status = new TraceValidationStatus(1, Activator.PLUGIN_ID);
        return status;
    }

    private static String getStringValue(ITmfEventField content, String fieldName) {
        ITmfEventField field = content.getField(fieldName);
        if (field == null) {
            return EMPTY;
        }
        Object val = field.getValue();
        if (!(val instanceof String)) {
            return EMPTY;
        }
        return (String) val;
    }

    @Override
    public synchronized @Nullable ITmfEvent getNext(@Nullable ITmfContext context) {
        if (context == null) {
            return null;
        }
        final ITmfContext savedContext = new TmfContext(context.getLocation(), context.getRank());
        CustomXmlEvent event = fTrace.getNext(context);
        if (event == null) {
            return null;
        }

        /* Translate the content of the event */
        /* The "fields" field contains a | separated list of field names */
        /* The "values" field contains a | separated list of field values */
        /* the "type" field contains a | separated list of field types */
        ITmfEventField content = event.getContent();
        if (content == null) {
            return null;
        }

        String fieldString = getStringValue(content, FIELD_NAMES_FIELD);
        String valueString = getStringValue(content, VALUES_FIELD);
        String typeString = getStringValue(content, TYPES_FIELD);

        String[] fields = fieldString.split(VALUES_SEPARATOR);
        String[] values = valueString.split(VALUES_SEPARATOR);
        String[] types = typeString.split(VALUES_SEPARATOR);
        ITmfEventField[] fieldsArray = new TmfEventField[fields.length];

        for (int i = 0; i < fields.length; i++) {
            String value = EMPTY;
            if (values.length > i) {
                value = values[i];
            }
            String type = null;
            if (types.length > i) {
                type = types[i];
            }
            Object val = value;
            if (type != null) {
                switch (type) {
                case TYPE_INTEGER: {
                    try {
                        val = Integer.valueOf(value);
                    } catch (NumberFormatException e) {
                        Activator.logError(String.format("Get next XML event: cannot cast value %s to integer", value), e); //$NON-NLS-1$
                        val = 0;
                    }
                    break;
                }
                case TYPE_LONG: {
                    try {
                        val = Long.valueOf(value);
                    } catch (NumberFormatException e) {
                        Activator.logError(String.format("Get next XML event: cannot cast value %s to long", value), e); //$NON-NLS-1$
                        val = 0L;
                    }
                    break;
                }
                case TYPE_LONG_ARRAY: {
                    try {
                        String[] split = value.split(",");
                        long[] arr = new long[split.length];
                        for (int j = 0; j < split.length; j++) {
                            arr[j] = Long.valueOf(split[j]);
                        }
                        val = arr;
                    } catch (NumberFormatException e) {
                        Activator.logError(String.format("Get next XML event: cannot cast one of the comma-separated values of %s to long", value), e); //$NON-NLS-1$
                        val = new long[0];
                    }
                    break;
                }
                case TYPE_INT_ARRAY: {
                    try {
                        String[] split = value.split(",");
                        int[] arr = new int[split.length];
                        for (int j = 0; j < split.length; j++) {
                            arr[j] = Integer.valueOf(split[j]);
                        }
                        val = arr;
                    } catch (NumberFormatException e) {
                        Activator.logError(String.format("Get next XML event: cannot cast one of the comma-separated values of %s to int", value), e); //$NON-NLS-1$
                        val = new int[0];
                    }
                    break;
                }
                default:
                    break;
                }
            }
            fieldsArray[i] = new TmfEventField(checkNotNull(fields[i]), val, null);
        }

        /*
         * Generate the aspects for this trace if it is the 'set_aspects'
         * definition
         */
        if (fTrace.getDefinition() != fDefinition) {
            generateAspects(fieldsArray);
            return null;
        }

        /* Create a new event with new fields and name */
        ITmfEventType customEventType = event.getType();
        String eventName = getStringValue(content, EVENT_NAME_FIELD);
        TmfEventType eventType = new TmfEventType(eventName, customEventType.getRootField());
        ITmfEventField eventFields = new CustomEventContent(content.getName(), content.getValue(), fieldsArray);
        /*
         * TODO: Timestamps for these traces are in nanos, but since the
         * CustomXmlTrace does not support this format, the timestamp of the
         * original is in second and we need to convert it. We should do that at
         * the source when it is supported
         */
        TmfEvent newEvent = new TmfEvent(this, ITmfContext.UNKNOWN_RANK, event.getTimestamp(), eventType, eventFields);
        updateAttributes(savedContext, event);
        return newEvent;
    }

    private void generateAspects(ITmfEventField[] fieldsArray) {
        ImmutableList.Builder<ITmfEventAspect<?>> builder = new ImmutableList.Builder<>();

        /* Initialize the first default trace aspects */
        builder.add(TmfBaseAspects.getTimestampAspect());
        builder.add(TmfBaseAspects.getEventTypeAspect());

        /* Add custom aspects in between */
        for (ITmfEventField field : fieldsArray) {
            String name = field.getName();
            final ITmfEventAspect<?> aspect = new TmfContentFieldAspect(name, name);
            if (name.equals(ASPECT_CPU)) {
                builder.add(new TmfCpuAspect() {
                    @Override
                    public @Nullable Integer resolve(ITmfEvent event) {
                        Object result = aspect.resolve(event);
                        if (result instanceof Number) {
                            return ((Number) result).intValue();
                        }
                        return null;
                    }
                });
            } else {
                builder.add(aspect);
            }
        }

        /* Add the big content aspect */
        builder.add(TmfBaseAspects.getContentsAspect());
        /* Add the additional aspects */
        builder.addAll(fAdditionalAspects);
        fAspects = builder.build();
    }

    @Override
    public Iterable<ITmfEventAspect<?>> getEventAspects() {
        return fAspects;
    }

    /**
     * Adds a new event aspect to this type of trace. Since this trace type is
     * used to build custom traces that mimic the behavior of real traces, the
     * required aspects may be missing and this method allows to add them.
     *
     * @param aspect
     *            The aspect to have
     */
    public void addEventAspect(ITmfEventAspect<?> aspect) {
        fAdditionalAspects.add(aspect);
        ImmutableSet.Builder<ITmfEventAspect<?>> builder = new ImmutableSet.Builder<>();
        builder.addAll(fAspects);
        builder.add(aspect);
        fAspects = builder.build();
    }

    /**
     * Add an additional new module
     *
     * @param module
     *            The new module
     */
    public void addAnalysisModule(IAnalysisModule module) {
        fAdditionalModules.add(module);
    }

    @Override
    public Iterable<@NonNull IAnalysisModule> getAnalysisModules() {
        @NonNull Iterable<IAnalysisModule> modules = super.getAnalysisModules();
        return checkNotNull(Iterables.concat(modules, fAdditionalModules));
    }

    @Override
    public @Nullable IAnalysisModule getAnalysisModule(@Nullable String analysisId) {
        Iterable<@NonNull IAnalysisModule> modules = getAnalysisModules();
        Optional<IAnalysisModule> opt = StreamSupport.stream(modules.spliterator(), false).filter(analysis -> analysis.getId().equals(analysisId)).findFirst();
        return opt.isPresent() ? opt.get() : null;
    }
}
