/*******************************************************************************
 * Copyright (c) 2012, 2019 Ericsson, École Polytechnique de Montréal
 * Copyright (c) 2010, 2011 Alexandre Montplaisir <alexandre.montplaisir@gmail.com>
 *
 * 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:
 *    Alexandre Montplaisir - Initial API and implementation
 *    Florian Wininger - Allow to change the size of a interval
 *    Patrick Tasse - Add message to exceptions
 *******************************************************************************/

package org.eclipse.tracecompass.internal.statesystem.core.backend.historytree;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Objects;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.datastore.core.encoding.HTVarInt;
import org.eclipse.tracecompass.datastore.core.serialization.ISafeByteBufferReader;
import org.eclipse.tracecompass.datastore.core.serialization.ISafeByteBufferWriter;
import org.eclipse.tracecompass.datastore.core.serialization.SafeByteBufferFactory;
import org.eclipse.tracecompass.internal.provisional.statesystem.core.statevalue.CustomStateValue;
import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue;
import org.eclipse.tracecompass.statesystem.core.statevalue.TmfStateValue;

/**
 * The interval component, which will be contained in a node of the History
 * Tree.
 *
 * @author Alexandre Montplaisir
 */
public final class HTInterval implements ITmfStateInterval {

    private static final Charset CHARSET = Charset.forName("UTF-8"); //$NON-NLS-1$

    private static final String errMsg = "Invalid interval data. Maybe your file is corrupt?"; //$NON-NLS-1$

    /* '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 static final byte TYPE_DOUBLE = 3;
    private static final byte TYPE_CUSTOM = 20;

    private final long fStart;
    private final long fDuration;
    private final int fAttribute;
    private final @Nullable Object fStateValue;

    /** Number of bytes used by this interval when it is written to disk */
    private int fSizeOnDisk;

    /**
     * Standard constructor
     *
     * Note: Unknown object type of the state value will be serialized
     * and deserialized as String.
     *
     * @param intervalStart
     *            Start time of the interval
     * @param intervalEnd
     *            End time of the interval
     * @param attribute
     *            Attribute (quark) to which the state represented by this
     *            interval belongs
     * @param value
     *            State value represented by this interval
     * @throws TimeRangeException
     *             If the start time or end time are invalid
     */
    public HTInterval(long intervalStart, long intervalEnd, int attribute,
            Object value) throws TimeRangeException {
        if (intervalStart > intervalEnd) {
            throw new TimeRangeException("Start:" + intervalStart + ", End:" + intervalEnd); //$NON-NLS-1$ //$NON-NLS-2$
        }

        fStart = intervalStart;
        fDuration = intervalEnd - intervalStart;
        fAttribute = attribute;
        fStateValue = (value instanceof TmfStateValue) ? ((ITmfStateValue) value).unboxValue() : value;
        fSizeOnDisk = computeSizeOnDisk(fStateValue, 0);
    }

    /**
     * Compute how much space (in bytes) an interval will take in its serialized
     * form on disk. This is dependent on its state value and the start time of
     * the node it is linked to.
     *
     * @param stateValue
     *            The state value
     * @param nodeStart
     *            The start time of the node the interval is linked to
     * @return The computed size on disk (with HTVarInt encoding)
     */
    private int computeSizeOnDisk(Object stateValue, long nodeStart) {
        /*
         * Minimum size is a 2x bytes (start), 2x bytes (duration), 1x int (attribute) and 1x
         * byte (value type).
         */
        int minSize = HTVarInt.getEncodedLengthLong(fStart - nodeStart) + HTVarInt.getEncodedLengthLong(fDuration) + Integer.BYTES + Byte.BYTES;

        if (stateValue == null) {
            return minSize;
        } else if (stateValue instanceof Integer) {
            return (minSize + Integer.BYTES);
        } else if (stateValue instanceof Long) {
            return (minSize + Long.BYTES);
        } else if (stateValue instanceof Double) {
            return (minSize + Double.BYTES);
        } else if (stateValue instanceof CustomStateValue) {
            /* Length of serialized value (short) + state value */
            return (minSize + Short.BYTES + ((CustomStateValue) stateValue).getSerializedSize());
        }
        String str = String.valueOf(stateValue);
        int strLength = str.getBytes(CHARSET).length;

        if (strLength > Short.MAX_VALUE) {
            throw new IllegalArgumentException("String is too long to be stored in state system: " + str); //$NON-NLS-1$
        }

        /*
         * String's length + 3 (2 bytes for size, 1 byte for \0 at the end)
         */
        return (minSize + strLength + 3);
    }

    /**
     * "Faster" constructor for inner use only. When we build an interval when
     * reading it from disk (with {@link #readFrom}), we already know the size
     * of the strings entry, so there is no need to call
     * {@link #computeStringsEntrySize()} and do an extra copy.
     */
    private HTInterval(long intervalStart, long intervalEnd, int attribute,
            Object value, int size) throws TimeRangeException {
        if (intervalStart > intervalEnd) {
            throw new TimeRangeException("Start:" + intervalStart + ", End:" + intervalEnd); //$NON-NLS-1$ //$NON-NLS-2$
        }

        fStart = intervalStart;
        fDuration = intervalEnd - intervalStart;
        fAttribute = attribute;
        fStateValue = value;
        fSizeOnDisk = size;
    }

    /**
     * Reader factory method. Builds the interval using an already-allocated
     * ByteBuffer, which normally comes from a NIO FileChannel.
     *
     * The interval is just a start, end, attribute and value, this is the
     * layout of the HTInterval on disk
     * <ul>
     * <li>start (2-9 bytes)</li>
     * <li>end (2-9 bytes)</li>
     * <li>attribute (4 bytes)</li>
     * <li>sv type (1 byte)</li>
     * <li>sv ( 0 bytes for null, 4 for int , 8 for long and double, and the
     * length of the string +2 for strings (it's variable))</li>
     * </ul>
     *
     * @param buffer
     *            The ByteBuffer from which to read the information
     * @param nodeStart
     *             The start time of the node this interval is linked to
     * @return The interval object
     * @throws IOException
     *             If there was an error reading from the buffer
     */
    public static final HTInterval readFrom(ByteBuffer buffer, long nodeStart) throws IOException {
        Object value;

        int posStart = buffer.position();
        /* Read the Data Section entry */
        long intervalStart = HTVarInt.readLong(buffer) + nodeStart;
        long intervalEnd = HTVarInt.readLong(buffer) + intervalStart;
        int attribute = buffer.getInt();

        /* Read the 'type' of the value, then react accordingly */
        byte valueType = buffer.get();
        switch (valueType) {

        case TYPE_NULL:
            value = null;
            break;

        case TYPE_INTEGER:
            value = buffer.getInt();
            break;

        case TYPE_STRING: {
            /* the first short = the size to read */
            int valueSize = buffer.getShort();

            byte[] array = new byte[valueSize];
            buffer.get(array);
            value = new String(array, CHARSET);

            /* Confirm the 0'ed byte at the end */
            byte res = buffer.get();
            if (res != 0) {
                throw new IOException(errMsg);
            }
            break;
        }

        case TYPE_LONG:
            /* Go read the matching entry in the Strings section of the block */
            value = buffer.getLong();
            break;

        case TYPE_DOUBLE:
            /* Go read the matching entry in the Strings section of the block */
            value = buffer.getDouble();
            break;

        case TYPE_CUSTOM: {
            short valueSize = buffer.getShort();
            ISafeByteBufferReader safeBuffer = SafeByteBufferFactory.wrapReader(buffer, valueSize);
            value = CustomStateValue.readSerializedValue(safeBuffer);
            break;
        }
        default:
            /* Unknown data, better to not make anything up... */
            throw new IOException(errMsg);
        }

        try {
            return new HTInterval(intervalStart, intervalEnd, attribute, value, buffer.position() - posStart);
        } catch (TimeRangeException e) {
            throw new IOException(errMsg);
        }
    }

    /**
     * Antagonist of the previous constructor, write the Data entry
     * corresponding to this interval in a ByteBuffer (mapped to a block in the
     * history-file, hopefully)
     *
     * The interval is just a start, end, attribute and value, this is the
     * layout of the HTInterval on disk
     * <ul>
     * <li>start (2-9 bytes)</li>
     * <li>end (2-9 bytes)</li>
     * <li>attribute (4 bytes)</li>
     * <li>sv type (1 byte)</li>
     * <li>sv ( 0 bytes for null, 4 for int , 8 for long and double, and the
     * length of the string +2 for strings (it's variable))</li>
     * </ul>
     *
     * @param buffer
     *            The already-allocated ByteBuffer corresponding to a SHT Node
     */
    public void writeInterval(ByteBuffer buffer, long nodeStart) {
        HTVarInt.writeLong(buffer, fStart - nodeStart);
        HTVarInt.writeLong(buffer, fDuration);
        buffer.putInt(fAttribute);

        if (fStateValue != null) {
            @NonNull Object value = fStateValue;
            if (value instanceof Integer) {
                buffer.put(TYPE_INTEGER);
                buffer.putInt((int) value);
            } else if (value instanceof Long) {
                buffer.put(TYPE_LONG);
                buffer.putLong((long) value);
            } else if (value instanceof Double) {
                buffer.put(TYPE_DOUBLE);
                buffer.putDouble((double) value);
            } else if (value instanceof CustomStateValue) {
                buffer.put(TYPE_CUSTOM);
                int size = ((CustomStateValue) value).getSerializedSize();
                buffer.putShort((short) size);
                ISafeByteBufferWriter safeBuffer = SafeByteBufferFactory.wrapWriter(buffer, size);
                ((CustomStateValue) value).serialize(safeBuffer);
            } else {
                String string = String.valueOf(value);
                buffer.put(TYPE_STRING);
                byte[] strArray = string.getBytes(CHARSET);

                /*
                 * Write the Strings entry (1st byte = size, then the bytes, then the 0). We
                 * have checked the string length at the constructor.
                 */
                buffer.putShort((short) strArray.length);
                buffer.put(strArray);
                buffer.put((byte) 0);
            }
        } else {
            buffer.put(TYPE_NULL);
        }
    }

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

    @Override
    public long getEndTime() {
        return fStart + fDuration;
    }

    @Override
    public int getAttribute() {
        return fAttribute;
    }

    @Override
    public ITmfStateValue getStateValue() {
        return TmfStateValue.newValue(fStateValue);
    }

    @Override
    public Object getValue() {
        return fStateValue;
    }

    @Override
    public boolean intersects(long timestamp) {
        return (fStart <= timestamp && (fStart + fDuration) >= timestamp);
    }

    /**
     * Total serialized size of this interval
     *
     * @return The interval size
     */
    public int getSizeOnDisk() {
        return fSizeOnDisk;
    }

    /**
     * Computes serialized size of this interval with linked node start time
     *
     * @param nodeStart
     *            The start time of the node the interval is linked to
     * @return The size of the interval on disk using the HTVarInt encoding
     */
    public int getSizeOnDisk(long nodeStart) {
        return computeSizeOnDisk(fStateValue, nodeStart);
    }

    /**
     * @param sizeOnDisk the sizeOnDisk to set
     */
    public void setSizeOnDisk(int sizeOnDisk) {
        fSizeOnDisk = sizeOnDisk;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        HTInterval other = (HTInterval) obj;
        return (fStart == other.fStart &&
                fDuration == other.fDuration &&
                fAttribute == other.fAttribute &&
                Objects.equals(fStateValue, other.fStateValue));
    }

    @Override
    public int hashCode() {
        return Objects.hash(fStart, fDuration, fAttribute, fStateValue);
    }

    @Override
    public String toString() {
        /* Only for debug, should not be externalized */
        StringBuilder sb = new StringBuilder();
        sb.append('[');
        sb.append(fStart);
        sb.append(", "); //$NON-NLS-1$
        sb.append(fStart + fDuration);
        sb.append(']');

        sb.append(", attribute = "); //$NON-NLS-1$
        sb.append(fAttribute);

        sb.append(", value = "); //$NON-NLS-1$
        sb.append(String.valueOf(fStateValue));

        return sb.toString();
    }
}
