SerialDataDecoder.java

package org.djutils.serialization;

import java.io.IOException;

import org.djunits.unit.Unit;
import org.djunits.value.vdouble.scalar.base.DoubleScalar;
import org.djunits.value.vfloat.scalar.base.FloatScalar;
import org.djutils.decoderdumper.Decoder;
import org.djutils.serialization.serializers.ArrayOrMatrixWithUnitSerializer;
import org.djutils.serialization.serializers.BasicPrimitiveArrayOrMatrixSerializer;
import org.djutils.serialization.serializers.FixedSizeObjectSerializer;
import org.djutils.serialization.serializers.Pointer;
import org.djutils.serialization.serializers.Serializer;
import org.djutils.serialization.serializers.StringArraySerializer;
import org.djutils.serialization.serializers.StringMatrixSerializer;

/**
 * Decoder for inspection of serialized data. The SerialDataDecoder implements a state machine that processes one byte at a
 * time. Output is sent to the buffer (a StringBuilder).
 * <p>
 * Copyright (c) 2013-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
 */
public class SerialDataDecoder implements Decoder
{
    /** The endian util to use to decode multi-byte values. */
    private final EndianUtil endianUtil;

    /** Type of the data that is currently being decoded. */
    private byte currentFieldType;

    /** The serializer for the <code>currentFieldType</code>. */
    private Serializer<?> currentSerializer = null;

    /** Position in the dataElementBytes where the next input byte shall be store. */
    private int nextDataElementByte = -1;

    /** Collects the bytes that constitute the current data element. */
    private byte[] dataElementBytes = new byte[0];

    /** Number of rows in an array or matrix. */
    private int rowCount;

    /** Number of columns in a matrix. */
    private int columnCount;

    /** Number of characters in a string. */
    private int charCount;

    /** Row of matrix or array that we are now reading. */
    private int currentRow;

    /** Column of matrix that we are now reading. */
    private int currentColumn;

    /** Character in the string that we are currently reading (either 8 or 16 bits). */
    private int currentChar;

    /** Djunits display unit. */
    private Unit<?> displayUnit;

    /** Array of units for array of column vectors. */
    private Unit<?>[] columnUnits = null;

    /** String builder for current output line. */
    private StringBuilder buffer = new StringBuilder();

    /**
     * Construct a new SerialDataDecoder.
     * @param endianUtil the endian util to use to decode multi-byte values
     */
    public SerialDataDecoder(final EndianUtil endianUtil)
    {
        this.endianUtil = endianUtil;
    }

    @Override
    public final String getResult()
    {
        String result = this.buffer.toString();
        this.buffer.setLength(0);
        return result;
    }

    @Override
    public final int getMaximumWidth()
    {
        return 80;
    }

    /**
     * Decode one (more) byte. This method must return true when a line becomes full due to this call, otherwise this method
     * must return false.
     * @param address the address that corresponds with the byte, for printing purposes.
     * @param theByte the byte to process
     * @return true if an output line has been completed by this call; false if at least one more byte can be appended to the
     *         local accumulator before the current output line is full
     * @throws IOException when the output device throws this exception
     */
    @Override
    public final boolean append(final int address, final byte theByte) throws IOException
    {
        boolean result = false;

        // check if first byte to indicate the field type
        if (this.currentSerializer == null)
        {
            result = processFieldTypeByte(theByte);
            return result;
        }

        // add byte to data element
        if (this.nextDataElementByte < this.dataElementBytes.length)
        {
            this.dataElementBytes[this.nextDataElementByte] = theByte;
        }
        this.nextDataElementByte++;
        // if data element complete, process it, and prepare for next data element (if any in current field type)
        if (this.nextDataElementByte == this.dataElementBytes.length)
        {
            result = processDataElement();
        }

        // are we done?
        if (this.currentSerializer == null)
        {
            return true;
        }
        return result;
    }

    /**
     * Process one byte that indicates the field type.
     * @param theByte the byte to process
     * @return whether line is full
     */
    private boolean processFieldTypeByte(final byte theByte)
    {
        this.currentFieldType = (byte) (theByte & 0x7F);
        this.currentSerializer = TypedObject.PRIMITIVE_DATA_DECODERS.get(this.currentFieldType);
        if (this.currentSerializer == null)
        {
            this.buffer.append(String.format("Error: Bad field type %02x - resynchronizing", this.currentFieldType));
            return true;
        }
        this.buffer.append(this.currentSerializer.dataClassName() + (this.currentSerializer.getNumberOfDimensions() > 0
                || this.currentSerializer.dataClassName().startsWith("Djunits") ? " " : ": "));

        this.columnCount = 0;
        this.rowCount = 0;
        this.displayUnit = null;
        this.columnUnits = null;

        // check the type and prepare for what is expected; primitive types
        if (this.currentSerializer instanceof FixedSizeObjectSerializer<?>)
        {
            var fsoe = (FixedSizeObjectSerializer<?>) this.currentSerializer;
            int size = fsoe.size(null);
            prepareForDataElement(size);
            return false;
        }

        // array or matrix type: next variable to expect is one or more ints (rows/cols)
        if (this.currentSerializer.getNumberOfDimensions() > 0)
        {
            int size = this.currentSerializer.getNumberOfDimensions() * 4;
            prepareForDataElement(size);
            return false;
        }

        // regular string type, next is an int for length
        if (this.currentFieldType == 9 || this.currentFieldType == 10)
        {
            prepareForDataElement(4);
            return false;
        }

        // djunits scalar type, next is quantity type and unit
        if (this.currentFieldType == 25 || this.currentFieldType == 26)
        {
            prepareForDataElement(2);
            return false;
        }

        this.buffer
                .append(String.format("Error: No field type handler for type %02x - resynchronizing", this.currentFieldType));
        return true;
    }

    /**
     * Process a completed data element. If the current data type has more elements, prepare for the next data element.
     * @return whether the line is full or not
     */
    private boolean processDataElement()
    {
        boolean result = false;

        // primitive types
        if (this.currentSerializer instanceof FixedSizeObjectSerializer<?>)
        {
            result = appendFixedSizeObject();
            done();
            return result;
        }

        // regular string type
        if (this.currentFieldType == 9 || this.currentFieldType == 10)
        {
            appendString();
            if (this.currentChar >= this.charCount)
            {
                done();
            }
            return false;
        }

        // processing of vector array type before array or matrix type
        if (this.currentFieldType == 31 || this.currentFieldType == 32)
        {
            if (this.rowCount == 0)
            {
                processRowsCols();
                prepareForDataElement(2 * this.columnCount); // unit type and display type for every column
                return false;
            }
            if (this.columnUnits == null)
            {
                return fillDjunitsVectorArrayColumnUnits();
            }
            return appendDjunitsVectorArrayElement();
        }

        // array or matrix type
        if (this.currentSerializer.getNumberOfDimensions() > 0)
        {
            if (this.rowCount == 0)
            {
                processRowsCols();
                if (this.currentSerializer.hasUnit())
                {
                    prepareForDataElement(2); // unit type and display type
                }
                else if (this.currentSerializer instanceof BasicPrimitiveArrayOrMatrixSerializer<?>)
                {
                    var bpams = (BasicPrimitiveArrayOrMatrixSerializer<?>) this.currentSerializer;
                    prepareForDataElement(bpams.getElementSize());
                }
                else if (this.currentSerializer instanceof StringArraySerializer
                        || this.currentSerializer instanceof StringMatrixSerializer)
                {
                    prepareForDataElement(4);
                }
                return false;
            }
            if (this.currentSerializer.hasUnit())
            {
                if (this.displayUnit == null)
                {
                    result = processUnit();
                    prepareForDataElement(((ArrayOrMatrixWithUnitSerializer<?, ?>) this.currentSerializer).getElementSize());
                    return result;
                }
                result = appendDjunitsElement();
                prepareForDataElement(this.dataElementBytes.length);
                incColumnCount();
                return result;
            }
            if (this.currentSerializer instanceof StringArraySerializer
                    || this.currentSerializer instanceof StringMatrixSerializer)
            {
                processStringElement();
                return false;
            }
            result = appendPrimitiveElement();
            prepareForDataElement(this.dataElementBytes.length);
            incColumnCount();
            return result;
        }

        // djunits scalar type
        if (this.currentFieldType == 25 || this.currentFieldType == 26)
        {
            if (this.displayUnit == null)
            {
                result = processUnit();
                prepareForDataElement(getSize() - 2); // subtract unit bytes
                return result;
            }
            result = appendDjunitsElement();
            done();
            return result;
        }

        // any leftovers?
        System.err.println("Did not process type " + this.currentFieldType);
        return true;
    }

    /**
     * Return the size of the encoding for a fixed size data element.
     * @return the encoding size of an element
     */
    private int getSize()
    {
        try
        {
            return this.currentSerializer.size(null);
        }
        catch (SerializationException e)
        {
            System.err.println("Could not determine size of element for field type " + this.currentFieldType);
            return 1;
        }
    }

    /**
     * Append a fixed size object (i.e., primitive type) to the buffer.
     * @return whether the line is full or not
     */
    private boolean appendFixedSizeObject()
    {
        try
        {
            Object value = this.currentSerializer.deSerialize(this.dataElementBytes, new Pointer(), this.endianUtil);
            this.buffer.append(value.toString());
        }
        catch (SerializationException e)
        {
            this.buffer.append("Error deserializing data");
            return true;
        }
        return false;
    }

    /**
     * Append a character of a string to the buffer.
     */
    private void appendString()
    {
        int elementSize = this.currentSerializer.dataClassName().contains("8") ? 1 : 2;
        if (this.charCount == 0)
        {
            this.charCount = this.endianUtil.decodeInt(this.dataElementBytes, 0);
            this.currentChar = 0;
            prepareForDataElement(elementSize);
        }
        else
        {
            if (elementSize == 1)
            {
                if (this.dataElementBytes[0] > 32 && this.dataElementBytes[0] < 127)
                {
                    this.buffer.append((char) this.dataElementBytes[0]); // safe to print
                }
                else
                {
                    this.buffer.append("."); // not safe to print
                }
            }
            else
            {
                char character = this.endianUtil.decodeChar(this.dataElementBytes, 0);
                if (Character.isAlphabetic(character))
                {
                    this.buffer.append(character); // safe to print
                }
                else
                {
                    this.buffer.append("."); // not safe to print
                }
            }
            this.currentChar++;
        }
        this.nextDataElementByte = 0;
    }

    /**
     * Process a string element in a string array or string matrix.
     */
    private void processStringElement()
    {
        appendString();
        if (this.currentChar >= this.charCount)
        {
            incColumnCount();
            this.charCount = 0;
            prepareForDataElement(4);
        }
    }

    /**
     * Process the height and width of a matrix, or length of an array.
     */
    private void processRowsCols()
    {
        this.currentRow = 0;
        this.currentColumn = 0;
        if (this.dataElementBytes.length == 8)
        {
            this.rowCount = this.endianUtil.decodeInt(this.dataElementBytes, 0);
            this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 4);
            this.buffer.append(String.format("height %d, width %d: ", this.rowCount, this.columnCount));
        }
        else
        {
            this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 0);
            this.rowCount = 1;
            this.buffer.append(String.format("length %d: ", this.columnCount));
        }
    }

    /**
     * Fill the unit type for the columns of this special type.
     * @return whether line is full or not.
     */
    private boolean fillDjunitsVectorArrayColumnUnits()
    {
        boolean result = false;
        this.columnUnits = new Unit<?>[this.columnCount];
        for (int i = 0; i < this.columnCount; i++)
        {
            byte unitTypeCode = this.dataElementBytes[2 * i];
            byte displayUnitCode = this.dataElementBytes[2 * i + 1];
            this.columnUnits[i] = DisplayType.getUnit(unitTypeCode, displayUnitCode);
            if (this.columnUnits[i] == null && !result)
            {
                this.buffer.append(
                        String.format("Error: Could not find unit type %d, display unit %d", unitTypeCode, displayUnitCode));
                result = true;
            }
        }
        prepareForDataElement(this.currentFieldType == 31 ? 4 : 8);
        return result;
    }

    /**
     * Append one (row) element in an array of (column) vectors.
     * @return whether the line is full or not
     * @param <U> the unit type
     * @param <FS> the float scalar type
     * @param <DS> the double scalar type
     */
    @SuppressWarnings("unchecked")
    private <U extends Unit<U>, FS extends FloatScalar<U, FS>,
            DS extends DoubleScalar<U, DS>> boolean appendDjunitsVectorArrayElement()
    {
        boolean result = false;
        try
        {
            U unit = (U) this.columnUnits[this.currentColumn];
            if (this.currentFieldType == 31)
            {
                float f = this.endianUtil.decodeFloat(this.dataElementBytes, 0);
                FloatScalar<U, FS> afs = FloatScalar.instantiateAnonymous(f, unit.getStandardUnit());
                afs.setDisplayUnit(unit);
                this.buffer.append(afs.toDisplayString().replace(" ", "") + " ");
            }
            else
            {
                double d = this.endianUtil.decodeDouble(this.dataElementBytes, 0);
                DoubleScalar<U, DS> ads = DoubleScalar.instantiateAnonymous(d, unit.getStandardUnit());
                ads.setDisplayUnit(unit);
                this.buffer.append(ads.toDisplayString().replace(" ", "") + " ");
            }
        }
        catch (Exception e)
        {
            this.buffer.append("Error: Illegal element in vector array -- could not parse");
            result = true;
        }
        prepareForDataElement(this.dataElementBytes.length);
        incColumnCount();
        return result;
    }

    /**
     * Process a unit (2 bytes).
     * @return whether the line is full or not
     */
    private boolean processUnit()
    {
        byte unitTypeCode = this.dataElementBytes[0];
        byte displayUnitCode = this.dataElementBytes[1];
        this.displayUnit = DisplayType.getUnit(unitTypeCode, displayUnitCode);
        if (this.displayUnit == null)
        {
            this.buffer
                    .append(String.format("Error: Could not find unit ype %d, display unit %d", unitTypeCode, displayUnitCode));
            return true;
        }
        return false;
    }

    /**
     * Process one element of a djunits vector or array.
     * @return whether the line is full or not
     * @param <U> the unit type
     * @param <FS> the float scalar type
     * @param <DS> the double scalar type
     */
    @SuppressWarnings("unchecked")
    private <U extends Unit<U>, FS extends FloatScalar<U, FS>, DS extends DoubleScalar<U, DS>> boolean appendDjunitsElement()
    {
        boolean result = false;
        try
        {
            if (this.dataElementBytes.length == 4)
            {
                float f = this.endianUtil.decodeFloat(this.dataElementBytes, 0);
                FloatScalar<U, FS> afs = FloatScalar.instantiateAnonymous(f, this.displayUnit.getStandardUnit());
                afs.setDisplayUnit((U) this.displayUnit);
                this.buffer.append(afs.toDisplayString().replace(" ", "") + " ");
            }
            else
            {
                double d = this.endianUtil.decodeDouble(this.dataElementBytes, 0);
                DoubleScalar<U, DS> ads = DoubleScalar.instantiateAnonymous(d, this.displayUnit.getStandardUnit());
                ads.setDisplayUnit((U) this.displayUnit);
                this.buffer.append(ads.toDisplayString().replace(" ", "") + " ");
            }
        }
        catch (Exception e)
        {
            this.buffer.append("Error: Could not instantiate djunits element");
            result = true;
        }
        return result;
    }

    /**
     * Append a primitive element to the buffer.
     * @return whether the line is full or not.
     */
    private boolean appendPrimitiveElement()
    {
        boolean result = false;
        this.buffer.append(switch (this.currentSerializer.fieldType())
        {
            // @formatter:off
            case FieldTypes.BYTE_8_ARRAY, FieldTypes.BYTE_8_MATRIX -> 
                String.format("%02x ", this.dataElementBytes[0]);
            case FieldTypes.SHORT_16_ARRAY, FieldTypes.SHORT_16_MATRIX -> 
                String.format("%d ", this.endianUtil.decodeShort(this.dataElementBytes, 0));
            case FieldTypes.INT_32_ARRAY, FieldTypes.INT_32_MATRIX -> 
                String.format("%d ", this.endianUtil.decodeInt(this.dataElementBytes, 0));
            case FieldTypes.LONG_64_ARRAY, FieldTypes.LONG_64_MATRIX -> 
                String.format("%d ", this.endianUtil.decodeLong(this.dataElementBytes, 0));
            case FieldTypes.FLOAT_32_ARRAY, FieldTypes.FLOAT_32_MATRIX -> 
                String.format("%f ", this.endianUtil.decodeFloat(this.dataElementBytes, 0));
            case FieldTypes.DOUBLE_64_ARRAY, FieldTypes.DOUBLE_64_MATRIX -> 
                String.format("%f ", this.endianUtil.decodeDouble(this.dataElementBytes, 0));
            case FieldTypes.BOOLEAN_8_ARRAY, FieldTypes.BOOLEAN_8_MATRIX -> 
                this.dataElementBytes[0] == 0 ? "false " : "true ";
            // @formatter:on
            default -> "Error: Unhandled type of basicPrimitiveArraySerializer: " + this.currentSerializer.fieldType();
        });
        return result;
    }

    /**
     * Increase the column count and possibly row count. Reset when complete.
     */
    private void incColumnCount()
    {
        this.currentColumn++;
        if (this.currentColumn >= this.columnCount)
        {
            this.currentColumn = 0;
            this.currentRow++;
            if (this.currentRow >= this.rowCount)
            {
                done();
            }
        }
    }

    /**
     * Reset the state when done with the current variable.
     */
    private void done()
    {
        this.currentSerializer = null;
        this.rowCount = 0;
        this.columnCount = 0;
        this.charCount = 0;
    }

    /**
     * Allocate a buffer for the next data element (or two).
     * @param dataElementSize size of the buffer
     */
    private void prepareForDataElement(final int dataElementSize)
    {
        this.dataElementBytes = new byte[dataElementSize];
        this.nextDataElementByte = 0;
    }

    @Override
    public final boolean ignoreForIdenticalOutputCheck()
    {
        return false;
    }

}