/*******************************************************************************
 * Copyright (c) 2016 É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
 *******************************************************************************/

package org.eclipse.tracecompass.internal.provisional.statesystem.core.statevalue;

import java.nio.BufferOverflowException;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.datastore.core.serialization.ISafeByteBufferReader;
import org.eclipse.tracecompass.datastore.core.serialization.ISafeByteBufferWriter;
import org.eclipse.tracecompass.internal.statesystem.core.Activator;
import org.eclipse.tracecompass.statesystem.core.statevalue.TmfStateValue;

import com.google.common.annotations.VisibleForTesting;

/**
 * This allows to define custom state values.
 *
 * Each sub-class should define a {@link CustomStateValueFactory} that has to be
 * registered, for example by the analysis using this state value type, through
 * the
 * {@link CustomStateValue#registerCustomFactory(byte, CustomStateValueFactory)}
 * method.
 *
 * Note to implementers: These state values are meant to be used in data
 * structures that save information on objects that varies in time, like a state
 * system. It will often be made persistent on disk at some point, so it is
 * suggested to make the child classes immutable.
 *
 * Data structures using these custom values will often keep the values in a
 * transient state before they are sent to be persisted. For persistence, it
 * will request the size of the value, then write its bytes to a buffer. Once
 * the size is requested, the value is about to be saved, so it is important
 * that its value and size do not change after that, as it may get in an
 * incoherent state, or, worse throw runtime exceptions.
 *
 * @author Geneviève Bastien
 * @since 2.0
 */
public abstract class CustomStateValue extends TmfStateValue {

    /* Minimum size of the state value */
    private static final int MIN_SIZE = Byte.BYTES;

    /**
     * Custom value factory interface. Each custom state value should have a
     * corresponding factory to re-create the object from a ByteBuffer
     */
    @FunctionalInterface
    public interface CustomStateValueFactory {

        /**
         * Get the custom state value from a byte buffer
         *
         * @param buffer
         *            the byte buffer
         * @return the custom state value
         */
        CustomStateValue readCustomValue(ISafeByteBufferReader buffer);
    }

    private static final CustomStateValueFactory[] CUSTOM_FACTORIES = new CustomStateValueFactory[256];

    /**
     * Register the custom factory that will be reused to create instances of
     * custom state value objects
     *
     * @param customId
     *            The ID of this custom type. For possible values of this ID,
     *            see {@link #getCustomTypeId()}
     * @param factory
     *            The factory used to create a new custom type object of this ID
     */
    public static void registerCustomFactory(byte customId, CustomStateValueFactory factory) {
        if (customId >= 0 && customId <= 20) {
            throw new IllegalArgumentException("State value IDs between 0 and 20 are reserved for built-in state value types"); //$NON-NLS-1$
        }
        CustomStateValueFactory currentFactory = CUSTOM_FACTORIES[customId - Byte.MIN_VALUE];
        if (currentFactory == null) {
            CUSTOM_FACTORIES[customId - Byte.MIN_VALUE] = factory;
        } else if (currentFactory != factory) {
            throw new IllegalStateException("Already a custom factory registered for " + Byte.toString(customId)); //$NON-NLS-1$
        }
    }

    /**
     * Unregisters the custom factory
     *
     * @param customId
     *            The ID of this custom type
     */
    @VisibleForTesting
    protected static void unregisterCustomFactory(byte customId) {
        CUSTOM_FACTORIES[customId - Byte.MIN_VALUE] = null;
    }

    /**
     * Get the custom factory for a byte
     *
     * @param customId
     *            the custom factory key
     * @return the custom factory or null if none is registered for the key
     */
    public static @Nullable CustomStateValueFactory getCustomFactory(byte customId) {
        return CUSTOM_FACTORIES[customId - Byte.MIN_VALUE];
    }

    /**
     * Read a serialized value (obtained with the
     * {@link #serialize(ISafeByteBufferWriter)} method) into a real
     * {@link CustomStateValue} object.
     *
     * @param buffer
     *            The byte buffer to read from
     * @return The state value object
     */
    public static TmfStateValue readSerializedValue(ISafeByteBufferReader buffer) {
        /* the first byte = the custom type id */
        byte customType = buffer.get();
        CustomStateValueFactory customFactory = CustomStateValue.getCustomFactory(customType);
        if (customFactory == null) {
            Activator.getDefault().logWarning("Custom factory for type " + customType + " does not exist"); //$NON-NLS-1$ //$NON-NLS-2$
            return TmfStateValue.nullValue();
        }

        return customFactory.readCustomValue(buffer);
    }

    /**
     * Get custom type id. The value should be between {@link Byte#MIN_VALUE}
     * and {@link Byte#MAX_VALUE}, but not between {@code 0} and {@code 20} that
     * are reserved for built-in state value types.
     *
     * @return the custom type ID
     */
    protected abstract Byte getCustomTypeId();

    /**
     * Serialize this state value into the byte buffer. This method should only
     * write the payload of the state value and should match what will be read
     * by the factory when deserializing.
     *
     * This serialized value must contain all the payload of the state value
     *
     * @param buffer
     *            The ByteBuffer to write the values to
     */
    protected abstract void serializeValue(ISafeByteBufferWriter buffer);

    /**
     * Get the serialized size of this state value. The size must not exceed
     * {@link Short#MAX_VALUE - MIN_SIZE}, otherwise serialization might throw a
     * {@link BufferOverflowException}
     *
     * @return The serialized size of this state value
     */
    protected abstract int getSerializedValueSize();

    /**
     * Serialize this custom state value into a byte buffer. It calls the
     * serialization of the state value itself and adds the specific fields to
     * interpret that byte array.
     *
     * The format of the value is [custom type (byte)][payload]
     *
     * The total serialized size should never exceed {@link Short#MAX_VALUE}
     *
     * @param buffer
     *            The ByteBuffer to write the values to
     *
     * @throws BufferOverflowException
     *             If the serialized size of the value ends up larger than the
     *             maximum of {@link Short#MAX_VALUE} and the implementation has
     *             no way of handling it
     */
    public final void serialize(ISafeByteBufferWriter buffer) {
        buffer.put(getCustomTypeId());
        serializeValue(buffer);
    }

    /**
     * Get the serialized size of this state value. This size will be used to
     * allow the buffer that will be sent to the
     * {@link #serialize(ISafeByteBufferWriter)} method
     *
     * @return The size of the serialized value
     */
    public final int getSerializedSize() {
        int size = getSerializedValueSize();
        if (size > Short.MAX_VALUE - MIN_SIZE) {
            throw new ArrayIndexOutOfBoundsException("Serialized state value is larger than the maximum allowed size of " + (Short.MAX_VALUE - MIN_SIZE) + ": " + size); //$NON-NLS-1$ //$NON-NLS-2$
        }
        return size + MIN_SIZE;
    }

    @Override
    public final Type getType() {
        return Type.CUSTOM;
    }

    @Override
    public boolean isNull() {
        return false;
    }

    @Override
    public @Nullable Object unboxValue() {
        return this;
    }

}
