/*******************************************************************************
 * Copyright (c) 2016 Ecole Polytechnique de Montreal, 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
 ******************************************************************************/
package org.eclipse.tracecompass.internal.tmf.analysis.xml.core.segment;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.datastore.core.interval.IHTIntervalReader;
import org.eclipse.tracecompass.datastore.core.serialization.ISafeByteBufferWriter;
import org.eclipse.tracecompass.datastore.core.serialization.SafeByteBufferFactory;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.Activator;
import org.eclipse.tracecompass.segmentstore.core.IContentSegment;
import org.eclipse.tracecompass.segmentstore.core.ISegment;
import org.eclipse.tracecompass.segmentstore.core.segment.interfaces.INamedSegment;
import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue;
import org.eclipse.tracecompass.statesystem.core.statevalue.TmfStateValue;

/**
 * This class implements an XML Pattern Segment. This type of segment has
 * content and a default timestamp, which is the start time of the segment.
 *
 * @author Jean-Christian Kouame
 */
public class TmfXmlPatternSegment implements INamedSegment, IContentSegment {

    /**
     * The serial version UID
     */
    private static final long serialVersionUID = 3556323761465412078L;

    /* 'Byte' equivalent for state values types */
    private static final byte TYPE_NULL = -1;
    private static final byte TYPE_INTEGER = 0;
    private static final byte TYPE_STRING = 1;
    private static final byte TYPE_LONG = 2;

    private final long fStart;
    private final long fEnd;
    private final @NonNull String fSegmentName;
    private transient @NonNull Map<@NonNull String, @NonNull Object> fContent;

    /**
     * The reader for this segment class
     */
    public static final @NonNull IHTIntervalReader<@NonNull ISegment> READER = buffer -> {

        long start = buffer.getLong();
        long end = buffer.getLong();
        // Reading the now unused scale
        buffer.getInt();
        String segmentName = buffer.getString();
        int contentSize = buffer.getInt();

        final Map<@NonNull String, @NonNull Object> content = new HashMap<>();
        for (int i = 0; i < contentSize; i++) {
            String name = buffer.getString().intern();

            Byte type = buffer.get();
            Object value;
            switch (type) {
            case TYPE_INTEGER:
                value = buffer.getInt();
                break;
            case TYPE_LONG:
                value = buffer.getLong();
                break;
            case TYPE_STRING:
                value = buffer.getString().intern();
                break;
            default:
                value = null;
                Activator.logError("Read segment failed : Invalid data, value will be ignored"); //$NON-NLS-1$
            }
            if (value != null) {
                content.put(name, value);
            }
        }
        return new TmfXmlPatternSegment(start, end, segmentName, content);
    };

    /**
     * Constructs an XML pattern segment
     *
     * @param start
     *            Start time of the pattern segment
     * @param end
     *            End time of the pattern segment
     * @param segmentName
     *            Name of the pattern segment
     * @param fields
     *            Fields of the pattern segment
     */
    public TmfXmlPatternSegment(long start, long end, String segmentName, @NonNull Map<@NonNull String, @NonNull Object> fields) {
        fStart = start;
        fEnd = end;
        fSegmentName = String.valueOf(segmentName);
        fContent = Collections.unmodifiableMap(fields);
    }

    /**
     * Get the content of the pattern segment
     *
     * @return The content
     */
    @Override
    public Map<@NonNull String, @NonNull Object> getContent() {
        return fContent;
    }

    @Override
    public String getName() {
        return fSegmentName;
    }

    @Override
    public int compareTo(@NonNull ISegment o) {
        int ret = INamedSegment.super.compareTo(o);
        if (ret != 0) {
            return ret;
        }
        ret = IContentSegment.super.compareTo(o);
        if (ret != 0) {
            return ret;
        }
        return toString().compareTo(o.toString());
    }

    @Override
    public long getStart() {
        return fStart;
    }

    @Override
    public long getEnd() {
        return fEnd;
    }

    @Override
    public String toString() {
        return new StringBuilder(getClass().getSimpleName())
                .append(", [fTimestampStart=").append(getStart()) //$NON-NLS-1$
                .append(", fTimestampEnd=").append(getEnd()) //$NON-NLS-1$
                .append(", duration= ").append(getLength()) //$NON-NLS-1$
                .append(", fName=").append(getName()) //$NON-NLS-1$
                .append(", fContent=").append(getContent()) //$NON-NLS-1$
                .append("]").toString(); //$NON-NLS-1$
    }

    @Override
    public void writeSegment(@NonNull ISafeByteBufferWriter buffer) {

        buffer.putLong(fStart);
        buffer.putLong(fEnd);
        // Legacy support, was scale
        // FIXME: Ideally, we should version the writer and remove this value
        buffer.putInt(0);
        buffer.putString(fSegmentName);
        // Write the number of fields
        buffer.putInt(fContent.size());

        // Write the fields
        for (Map.Entry<String, Object> entry : fContent.entrySet()) {
            buffer.putString(entry.getKey());
            final Object value = entry.getValue();
            final byte type = getByteFromObject(value);
            buffer.put(type);
            switch (type) {
            case TYPE_NULL:
                break;
            case TYPE_INTEGER:
                buffer.putInt((Integer) value);
                break;
            case TYPE_LONG:
                buffer.putLong((Long) value);
                break;
            case TYPE_STRING:
                final String string = (String) value;
                buffer.putString(Objects.requireNonNull(string));
                break;
            default:
                Activator.logError("Write object failed : Invalid data"); //$NON-NLS-1$
            }
        }
    }

    @Override
    public int getSizeOnDisk() {
        int size = 2 * Long.BYTES + Integer.BYTES + SafeByteBufferFactory.getStringSizeInBuffer(fSegmentName) + Integer.BYTES;
        for (Map.Entry<String, Object> entry : fContent.entrySet()) {
            size += SafeByteBufferFactory.getStringSizeInBuffer(entry.getKey());
            final Object value = entry.getValue();
            final byte type = getByteFromObject(value);
            size += Byte.BYTES;
            switch (type) {
            case TYPE_NULL:
                break;
            case TYPE_INTEGER:
                size += Integer.BYTES;
                break;
            case TYPE_LONG:
                size += Long.BYTES;
                break;
            case TYPE_STRING:
                final String string = (String) value;
                size += SafeByteBufferFactory.getStringSizeInBuffer(Objects.requireNonNull(string));
                break;
            default:
                Activator.logError("get segment size on disk failed : Invalid data"); //$NON-NLS-1$
            }
        }
        return size;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();

        // Write the number of fields
        out.writeInt(fContent.size());

        // Write the fields
        for (Map.Entry<String, Object> entry : fContent.entrySet()) {
            out.writeInt(entry.getKey().length());
            out.writeBytes(entry.getKey());
            final Object value = entry.getValue();
            final byte type = getByteFromObject(value);
            out.writeByte(type);
            switch (type) {
            case TYPE_NULL:
                break;
            case TYPE_INTEGER:
                out.writeInt((Integer) value);
                break;
            case TYPE_LONG:
                out.writeLong((Long) value);
                break;
            case TYPE_STRING:
                final String string = (String) value;
                out.writeInt(string.length());
                out.writeBytes(string);
                break;
            default:
                throw new IOException("Write object failed : Invalid data"); //$NON-NLS-1$
            }
        }
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        int contentSize = in.readInt();

        final Map<@NonNull String, @NonNull Object> content = new HashMap<>();
        for (int i = 0; i < contentSize; i++) {
            int length = in.readInt();
            byte[] bytes = new byte[length];
            in.read(bytes, 0, length);
            String name = new String(bytes).intern();

            Byte type = in.readByte();
            ITmfStateValue value;
            switch (type) {
            case TYPE_NULL:
                value = TmfStateValue.nullValue();
                break;
            case TYPE_INTEGER:
                value = TmfStateValue.newValueInt(in.readInt());
                break;
            case TYPE_LONG:
                value = TmfStateValue.newValueLong(in.readLong());
                break;
            case TYPE_STRING:
                length = in.readInt();
                bytes = new byte[length];
                in.read(bytes, 0, length);
                value = TmfStateValue.newValueString(new String(bytes).intern());
                break;
            default:
                throw new IOException("Read object failed : Invalid data"); //$NON-NLS-1$
            }
            content.put(name, value);
        }
        fContent = content;
    }

    /**
     * Here we determine how state values "types" are written in the 8-bit field
     * that indicates the value type in the file.
     */
    private static byte getByteFromObject(Object value) {
        if (value == null) {
            return TYPE_NULL;
        }
        if (value instanceof Integer) {
            return TYPE_INTEGER;
        }
        if (value instanceof Long) {
            return TYPE_LONG;
        }
        if (value instanceof String) {
            return TYPE_STRING;
        }
        /* Should not happen, only the previous types are supported */
        throw new IllegalStateException("Data type " + value.getClass() + " not supported"); //$NON-NLS-1$ //$NON-NLS-2$
    }
}
