/*
 * Copyright 2014 OpenRQ Team
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.fec.openrq;


import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import net.fec.openrq.decoder.SourceBlockState;
import net.fec.openrq.parameters.FECParameters;
import net.fec.openrq.parameters.ParameterChecker;
import net.fec.openrq.util.checking.Indexables;
import net.fec.openrq.util.datatype.SizeOf;
import net.fec.openrq.util.datatype.UnsignedTypes;
import net.fec.openrq.util.io.BufferOperation;
import net.fec.openrq.util.io.ExtraChannels;


/**
 * This class represents information about a source block decoder that can be conveyed to an encoder, in order to help
 * decide which symbols are prioritized for encoding.
 * <p>
 * When a back communication channel is available from a receiver to a sender of data, information about a specific
 * source block may be useful in determining how the source block is encoded by the sender. For example, such
 * information may contain a list with the identifiers of the missing source symbols at the receiver, so that the sender
 * can know which symbols it needs to transmit.
 * <p>
 * This class offers the following information:
 * <ul>
 * <li><b>Source Block Number</b>: the identifier of the source block
 * <li><b>Decoder State</b>: a flag indicating if the source block is incomplete, decoded or if it suffered a decoding
 * failure
 * <li><b>Missing Source Symbols</b>: a list with the identifiers of all missing source symbols at the decoder
 * <li><b>Available Repair Symbols</b>: a list with the identifiers of all repair symbols that were received by the
 * decoder
 * </ul>
 * <p>
 * Methods are provided to write instances of this class to arrays of bytes, {@link ByteBuffer} objects, serializable
 * objects, {@link DataOutput} objects and {@link WritableByteChannel} objects. Additionally, static methods are
 * provided to parse/read instances of this class from arrays of bytes, {@code ByteBuffer} objects, serializable
 * objects, {@link DataInput} objects and {@link ReadableByteChannel} objects.
 */
public final class SBDInfo {

    /**
     * Requires valid arguments and unmodifiable sets!
     */
    @SuppressWarnings("javadoc")
    static SBDInfo newInformation(
        int sbn,
        SourceBlockState state,
        Set<Integer> missingSourceSymbols,
        Set<Integer> availableRepairSymbols)
    {

        return new SBDInfo(
            sbn,
            state,
            Collections.unmodifiableSet(missingSourceSymbols),
            Collections.unmodifiableSet(availableRepairSymbols));
    }

    /**
     * Parses source block decoder information from the given serializable object.
     * <p>
     * The provided FEC parameters are used to validate the information during the parsing.
     * <p>
     * The returned container object indicates if the parsing succeeded or failed:
     * <ul>
     * <li>If the parsing succeeded, the information can be retrieved by calling the method {@link Parsed#value()}
     * <li>If the parsing failed, the container object will be {@linkplain Parsed#isValid() invalid} and the reason for
     * the parsing failure can be retrieved by calling the method {@link Parsed#failureReason()} </ul>
     * 
     * @param serInfo
     *            A serializable object containing source block decoder information
     * @param fecParams
     *            FEC parameters associated to the source data containing the source block being decoded
     * @return a container object containing a {@code SBFInfo} instance or a parsing failure reason string
     * @exception NullPointerException
     *                If any argument is {@code null}
     */
    public static Parsed<SBDInfo> parse(SerializableSBDInfo serInfo, FECParameters fecParams) {

        return parse(serInfo.sourceBlockDecoderInfo(), fecParams);
    }

    /**
     * Parses source block decoder information from the given array. The information bytes in the array must follow the
     * format specified by {@link #asArray()}.
     * <p>
     * The information will be read, in the array, from position {@code 0} inclusive to position {@code array.length}
     * exclusive.
     * <p>
     * The provided FEC parameters are used to validate the information during the parsing.
     * <p>
     * The returned container object indicates if the parsing succeeded or failed:
     * <ul>
     * <li>If the parsing succeeded, the information can be retrieved by calling the method {@link Parsed#value()}
     * <li>If the parsing failed, the container object will be {@linkplain Parsed#isValid() invalid} and the reason for
     * the parsing failure can be retrieved by calling the method {@link Parsed#failureReason()} </ul>
     * 
     * @param array
     *            An array of bytes containing source block decoder information
     * @param fecParams
     *            FEC parameters associated to the source data containing the source block being decoded
     * @return a container object containing source block decoder information or a parsing failure reason string
     * @exception NullPointerException
     *                If any argument is {@code null}
     */
    public static Parsed<SBDInfo> parse(byte[] array, FECParameters fecParams) {

        return parse(array, 0, array.length, fecParams);
    }

    /**
     * Parses source block decoder information from the given array. The information bytes in the array must follow the
     * format specified by {@link #asArray()}.
     * <p>
     * The information will be read, in the array, from position {@code off} inclusive to position {@code (off + len)}
     * exclusive.
     * <p>
     * The provided FEC parameters are used to validate the information during the parsing.
     * <p>
     * The returned container object indicates if the parsing succeeded or failed:
     * <ul>
     * <li>If the parsing succeeded, the information can be retrieved by calling the method {@link Parsed#value()}
     * <li>If the parsing failed, the container object will be {@linkplain Parsed#isValid() invalid} and the reason for
     * the parsing failure can be retrieved by calling the method {@link Parsed#failureReason()} </ul>
     * 
     * @param array
     *            An array of bytes containing source block decoder information
     * @param off
     *            The starting index in the array (must be non-negative)
     * @param len
     *            The length of the information (must be non-negative and no larger than {@code array.length - off})
     * @param fecParams
     *            FEC parameters associated to the source data containing the source block being decoded
     * @return a container object containing source block decoder information or a parsing failure reason string
     * @exception IndexOutOfBoundsException
     *                If the pre-conditions on the array offset and length do not hold
     * @exception NullPointerException
     *                If any argument is {@code null}
     */
    public static Parsed<SBDInfo> parse(byte[] array, int off, int len, FECParameters fecParams) {

        Indexables.checkOffsetLengthBounds(off, len, array.length);
        return parse(ByteBuffer.wrap(array, off, len), fecParams);
    }

    /**
     * Parses source block decoder information from the given buffer. The information bytes in the buffer must follow
     * the format specified by {@link #asBuffer()}.
     * <p>
     * The information will be read, in the buffer, from the current {@linkplain ByteBuffer#position() position}
     * inclusive to the current {@linkplain ByteBuffer#limit() limit} exclusive. If the parsing succeeds, the position
     * of the buffer will have been advanced by the number of bytes read.
     * <p>
     * The provided FEC parameters are used to validate the information during the parsing.
     * <p>
     * The returned container object indicates if the parsing succeeded or failed:
     * <ul>
     * <li>If the parsing succeeded, the information can be retrieved by calling the method {@link Parsed#value()}
     * <li>If the parsing failed, the container object will be {@linkplain Parsed#isValid() invalid} and the reason for
     * the parsing failure can be retrieved by calling the method {@link Parsed#failureReason()} </ul>
     * 
     * @param buffer
     *            A buffer containing source block decoder information
     * @param fecParams
     *            FEC parameters associated to the source data containing the source block being decoded
     * @return a container object containing source block decoder information or a parsing failure reason string
     * @exception NullPointerException
     *                If any argument is {@code null}
     */
    public static Parsed<SBDInfo> parse(ByteBuffer buffer, FECParameters fecParams) {

        Objects.requireNonNull(buffer);

        try {
            final int sbn = readSBN(buffer, fecParams);
            final int K = DataUtils.getK(fecParams, sbn);

            final SourceBlockState state = readState(buffer);

            final int numMiss = readNumMissingSourceSymbols(buffer, K, state);
            final Set<Integer> missing = readMissingSourceSymbols(buffer, numMiss, K);

            final int numAvail = readNumAvailableRepairSymbols(buffer, K, state);
            final Set<Integer> available = readAvailableRepairSymbols(buffer, numAvail, K);

            return newRemoteInfo(sbn, state, missing, available);
        }
        catch (InternalParsingException e) {
            return Parsed.invalid(e.getMessage());
        }
    }

    /**
     * Reads and parses source block decoder information from a {@code DataInput} object. The read information bytes
     * must follow the format specified by {@link #writeTo(java.io.DataOutput)}.
     * <p>
     * Examples of {@code DataInput} objects are {@link java.io.DataInputStream DataInputStream} and
     * {@link java.io.ObjectInputStream ObjectInputStream}.
     * <p>
     * The provided FEC parameters are used to validate the information during the parsing.
     * <p>
     * The returned container object indicates if the parsing succeeded or failed:
     * <ul>
     * <li>If the parsing succeeded, the information can be retrieved by calling the method {@link Parsed#value()}
     * <li>If the parsing failed, the container object will be {@linkplain Parsed#isValid() invalid} and the reason for
     * the parsing failure can be retrieved by calling the method {@link Parsed#failureReason()} </ul>
     * <p>
     * <b><em>Blocking behavior</em></b>: this method blocks until the whole information is read from the input, or a
     * parsing failure is detected, or an {@code IOException} is throw.
     * 
     * @param in
     *            A {@code DataInput} object from which source block decoder information is read
     * @param fecParams
     *            FEC parameters associated to the source data containing the source block being decoded
     * @return a container object containing source block decoder information or a parsing failure reason string
     * @throws IOException
     *             If an I/O error occurs while reading from the {@code DataInput} object
     * @exception NullPointerException
     *                If any argument is {@code null}
     */
    public static Parsed<SBDInfo> readFrom(DataInput in, FECParameters fecParams) throws IOException {

        Objects.requireNonNull(in);

        try {
            final int sbn = readSBN(in, fecParams);
            final int K = DataUtils.getK(fecParams, sbn);

            final SourceBlockState state = readState(in);

            final int numMiss = readNumMissingSourceSymbols(in, K, state);
            final Set<Integer> missing = readMissingSourceSymbols(in, numMiss, K);

            final int numAvail = readNumAvailableRepairSymbols(in, K, state);
            final Set<Integer> available = readAvailableRepairSymbols(in, numAvail, K);

            return newRemoteInfo(sbn, state, missing, available);
        }
        catch (InternalParsingException e) {
            return Parsed.invalid(e.getMessage());
        }
    }

    /**
     * Reads and parses source block decoder information from a {@code ReadableByteChannel} object. The read information
     * bytes must follow the format specified by {@link #writeTo(java.nio.channels.WritableByteChannel)}.
     * <p>
     * Examples of {@code ReadableByteChannel} objects are {@link java.nio.channels.SocketChannel SocketChannel} and
     * {@link java.nio.channels.FileChannel FileChannel}.
     * <p>
     * The returned container object indicates if the parsing succeeded or failed:
     * <ul>
     * <li>If the parsing succeeded, the information can be retrieved by calling the method {@link Parsed#value()}
     * <li>If the parsing failed, the container object will be {@linkplain Parsed#isValid() invalid} and the reason for
     * the parsing failure can be retrieved by calling the method {@link Parsed#failureReason()} </ul>
     * <p>
     * <b><em>Blocking behavior</em></b>: this method blocks until the whole information is read from the channel, or a
     * parsing failure is detected, or an {@code IOException} is throw.
     * 
     * @param ch
     *            A {@code ReadableByteChannel} object from which source block decoder information is read
     * @param fecParams
     *            FEC parameters associated to the source data containing the source block being decoded
     * @return a container object containing source block decoder information or a parsing failure reason string
     * @throws IOException
     *             If an I/O error occurs while reading from the {@code ReadableByteChannel} object
     * @exception NullPointerException
     *                If any argument is {@code null}
     */
    public static Parsed<SBDInfo> readFrom(ReadableByteChannel ch, FECParameters fecParams) throws IOException {

        try {
            final ByteBuffer sbnAndStateBuf = ByteBuffer.allocate(SizeOf.BYTE + SizeOf.BYTE);
            ExtraChannels.readBytes(ch, sbnAndStateBuf, BufferOperation.FLIP_ABSOLUTELY);

            final int sbn = readSBN(sbnAndStateBuf, fecParams);
            final int K = DataUtils.getK(fecParams, sbn);
            final SourceBlockState state = readState(sbnAndStateBuf);

            final ByteBuffer numMissBuf = ByteBuffer.allocate(SizeOf.SHORT);
            ExtraChannels.readBytes(ch, numMissBuf, BufferOperation.FLIP_ABSOLUTELY);

            final int numMiss = readNumMissingSourceSymbols(numMissBuf, K, state);

            final ByteBuffer missingBuf = ByteBuffer.allocate(numMiss * SizeOf.SHORT);
            ExtraChannels.readBytes(ch, missingBuf, BufferOperation.FLIP_ABSOLUTELY);

            final Set<Integer> missing = readMissingSourceSymbols(missingBuf, numMiss, K);

            final ByteBuffer numAvailBuf = ByteBuffer.allocate(SizeOf.UNSIGNED_3_BYTES);
            ExtraChannels.readBytes(ch, numAvailBuf, BufferOperation.FLIP_ABSOLUTELY);

            final int numAvail = readNumAvailableRepairSymbols(numAvailBuf, K, state);

            final ByteBuffer availableBuf = ByteBuffer.allocate(numAvail * SizeOf.UNSIGNED_3_BYTES);
            ExtraChannels.readBytes(ch, availableBuf, BufferOperation.FLIP_ABSOLUTELY);

            final Set<Integer> available = readAvailableRepairSymbols(availableBuf, numAvail, K);

            return newRemoteInfo(sbn, state, missing, available);
        }
        catch (InternalParsingException e) {
            return Parsed.invalid(e.getMessage());
        }
    }

    private static Parsed<SBDInfo> newRemoteInfo(
        int sbn,
        SourceBlockState state,
        Set<Integer> missingSourceSymbols,
        Set<Integer> availableRepairSymbols)
    {

        return Parsed.of(
            new SBDInfo(
                sbn,
                state,
                Collections.unmodifiableSet(missingSourceSymbols),
                Collections.unmodifiableSet(availableRepairSymbols)));
    }


    private final int sbn;
    private final SourceBlockState state;
    private final Set<Integer> missingSourceSymbols;
    private final Set<Integer> availableRepairSymbols;


    /*
     * Requires unmodifiable sets!
     */
    private SBDInfo(
        int sbn,
        SourceBlockState state,
        Set<Integer> missingSourceSymbols,
        Set<Integer> availableRepairSymbols)
    {

        this.sbn = sbn;
        this.state = Objects.requireNonNull(state);
        this.missingSourceSymbols = Objects.requireNonNull(missingSourceSymbols);
        this.availableRepairSymbols = Objects.requireNonNull(availableRepairSymbols);
    }

    /**
     * Returns the identifier of the source block being decoded.
     * 
     * @return the identifier of the source block being decoded
     */
    public int sourceBlockNumber() {

        return sbn;
    }

    /**
     * Returns the latest state of the source block being decoded.
     * <p>
     * The result of this method invocation is a {@link SourceBlockState} value:
     * <dl>
     * <dt>{@link SourceBlockState#INCOMPLETE INCOMPLETE}:</dt>
     * <dd>means that not enough encoding symbols are available for a decoding operation.</dd>
     * <dt>{@link SourceBlockState#DECODED DECODED}:</dt>
     * <dd>means that a decoding operation took place and succeeded in decoding the source block.</dd>
     * <dt>{@link SourceBlockState#DECODING_FAILURE DECODING_FAILURE}:</dt>
     * <dd>means that a decoding operation took place but failed in decoding the source block; additional encoding
     * symbols are required for a successful decoding.</dd>
     * </dl>
     * 
     * @return the latest state of the source block being decoded
     */
    public SourceBlockState state() {

        return state;
    }

    /**
     * Returns an unmodifiable set of integers containing the encoding symbol identifiers of the missing source symbols
     * from the source block being decoded.
     * 
     * @return a set of encoding symbol identifiers of missing source symbols
     */
    public Set<Integer> missingSourceSymbols() {

        return missingSourceSymbols;
    }

    /**
     * Returns an unmodifiable set of integers containing the encoding symbol identifiers of the available repair
     * symbols for decoding.
     * 
     * @return a set of encoding symbol identifiers of available repair symbols
     */
    public Set<Integer> availableRepairSymbols() {

        return availableRepairSymbols;
    }

    /**
     * Returns {@code true} if, and only if, this instance is equal to another object.
     * <p>
     * This instance ({@code this}) is equal to another object ({@code obj}), if and only if:
     * <ul>
     * <li>{@code obj} is non-null
     * <li>and {@code obj} is an instance of {@code SBDInfo} <li>and {@code this}.{@link #sourceBlockNumber()} ==
     * {@code obj.sourceBlockNumber()} <li>and {@code this}.{@link #state()}{@code .equals(obj.state())} <li>and
     * {@code this}.{@link #missingSourceSymbols()}{@code .equals(obj.missingSourceSymbols())} <li>and {@code this}.
     * {@link #availableRepairSymbols()}{@code .equals(obj.availableRepairSymbols())} </ul>
     */
    @Override
    public boolean equals(Object obj) {

        return obj instanceof SBDInfo && areEqual(this, (SBDInfo)obj);
    }

    private static boolean areEqual(SBDInfo info1, SBDInfo info2) {

        return info1.sbn == info2.sbn &&
               info1.state.equals(info2.state) &&
               info1.missingSourceSymbols.equals(info2.missingSourceSymbols) &&
               info1.availableRepairSymbols.equals(info2.availableRepairSymbols);
    }

    /**
     * Returns a hash code value based on the whole information of this instance.
     */
    @Override
    public int hashCode() {

        final int prime = 31;
        int result = 1;
        result = prime * result + sbn;
        result = prime * result + state.hashCode();
        result = prime * result + missingSourceSymbols.hashCode();
        result = prime * result + availableRepairSymbols.hashCode();
        return result;
    }

    @Override
    public String toString() {

        return String.format("SBN: %d%nState: %s%nMissing source symbols: %s%nAvailable repair symbols: %s",
            sbn, state, missingSourceSymbols, availableRepairSymbols);
    }

    /**
     * Returns a serializable object with this information encoded in a compact format (the same format as of method
     * {@link #asArray()}).
     * 
     * @return a serializable object with this information encoded in a compact format
     */
    public SerializableSBDInfo asSerializable() {

        return new SerializableSBDInfo(asArray());
    }

    /**
     * Returns an array with this information encoded in a compact format.
     * <p>
     * Let:
     * <ul>
     * <li>{@code NUM_MISSING_BYTES} = {@code 2} &times; {@link #missingSourceSymbols()}{@code .size()} <li>
     * {@code NUM_AVAILABLE_BYTES} = {@code 3} &times; {@link #availableRepairSymbols()}{@code .size()} </ul>
     * <p>
     * The array will contain the {@linkplain #sourceBlockNumber() source block number}, followed by a byte value
     * corresponding to the {@linkplain #state() latest state of the source block}, followed by
     * {@code NUM_MISSING_BYTES}, followed by the list of the encoding symbol identifiers (ESIs) of the
     * {@linkplain #missingSourceSymbols() missing source symbols}, followed by {@code NUM_AVAILABLE_BYTES}, followed by
     * the list of the ESIs of the {@linkplain #availableRepairSymbols() available repair symbols}.
     * 
     * @return an array with this information encoded in a compact format
     */
    public byte[] asArray() {

        final byte[] array = new byte[getEncodedByteSize()];
        writeTo(ByteBuffer.wrap(array));

        return array;
    }

    /**
     * Writes in the provided array starting at index zero this information encoded in a compact format.
     * <p>
     * Let:
     * <ul>
     * <li>{@code NUM_MISSING_BYTES} = {@code 2} &times; {@link #missingSourceSymbols()}{@code .size()} <li>
     * {@code NUM_AVAILABLE_BYTES} = {@code 3} &times; {@link #availableRepairSymbols()}{@code .size()} </ul>
     * <p>
     * The write consists of the {@linkplain #sourceBlockNumber() source block number}, followed by a byte value
     * corresponding to the {@linkplain #state() latest state of the source block}, followed by
     * {@code NUM_MISSING_BYTES}, followed by the list of the encoding symbol identifiers (ESIs) of the
     * {@linkplain #missingSourceSymbols() missing source symbols}, followed by {@code NUM_AVAILABLE_BYTES}, followed by
     * the list of the ESIs of the {@linkplain #availableRepairSymbols() available repair symbols}.
     * <p>
     * The provided array must have a length of at least {@code (7 + NUM_MISSING_BYTES + NUM_AVAILABLE_BYTES)} bytes.
     * 
     * @param array
     *            An array on which the information is written
     * @exception IndexOutOfBoundsException
     *                If the length of the array is insufficient to hold the information
     * @exception NullPointerException
     *                If the provided array is {@code null}
     */
    public void writeTo(byte[] array) {

        writeTo(array, 0);
    }

    /**
     * Writes in the provided array starting in a specific index this information encoded in a compact format.
     * <p>
     * Let:
     * <ul>
     * <li>{@code NUM_MISSING_BYTES} = {@code 2} &times; {@link #missingSourceSymbols()}{@code .size()} <li>
     * {@code NUM_AVAILABLE_BYTES} = {@code 3} &times; {@link #availableRepairSymbols()}{@code .size()} </ul>
     * <p>
     * The write consists of the {@linkplain #sourceBlockNumber() source block number}, followed by a byte value
     * corresponding to the {@linkplain #state() latest state of the source block}, followed by
     * {@code NUM_MISSING_BYTES}, followed by the list of the encoding symbol identifiers (ESIs) of the
     * {@linkplain #missingSourceSymbols() missing source symbols}, followed by {@code NUM_AVAILABLE_BYTES}, followed by
     * the list of the ESIs of the {@linkplain #availableRepairSymbols() available repair symbols}.
     * <p>
     * The provided array must have at least {@code (7 + NUM_MISSING_BYTES + NUM_AVAILABLE_BYTES)} bytes between the
     * given index and its length.
     * 
     * @param array
     *            An array on which the information is written
     * @param offset
     *            The starting array index at which the information is written (must be non-negative)
     * @exception IndexOutOfBoundsException
     *                If the offset is negative or if the length of the array region starting at the offset is
     *                insufficient to hold the information
     * @exception NullPointerException
     *                If the provided array is {@code null}
     */
    public void writeTo(byte[] array, int offset) {

        final int arraySize = getEncodedByteSize();
        if (offset < 0 || array.length - offset < arraySize) throw new IndexOutOfBoundsException();
        writeTo(ByteBuffer.wrap(array, offset, arraySize));
    }

    /**
     * Returns a buffer with this information encoded in a compact format.
     * <p>
     * Let:
     * <ul>
     * <li>{@code NUM_MISSING_BYTES} = {@code 2} &times; {@link #missingSourceSymbols()}{@code .size()} <li>
     * {@code NUM_AVAILABLE_BYTES} = {@code 3} &times; {@link #availableRepairSymbols()}{@code .size()} </ul>
     * <p>
     * The buffer will contain the {@linkplain #sourceBlockNumber() source block number}, followed by a byte value
     * corresponding to the {@linkplain #state() latest state of the source block}, followed by
     * {@code NUM_MISSING_BYTES}, followed by the list of the encoding symbol identifiers (ESIs) of the
     * {@linkplain #missingSourceSymbols() missing source symbols}, followed by {@code NUM_AVAILABLE_BYTES}, followed by
     * the list of the ESIs of the {@linkplain #availableRepairSymbols() available repair symbols}.
     * 
     * @return a buffer with this information encoded in a compact format
     */
    public ByteBuffer asBuffer() {

        final ByteBuffer buffer = ByteBuffer.allocate(getEncodedByteSize());
        writeTo(buffer);
        buffer.flip();

        return buffer;
    }

    /**
     * Writes in the provided buffer this information encoded in a compact format.
     * <p>
     * Let:
     * <ul>
     * <li>{@code NUM_MISSING_BYTES} = {@code 2} &times; {@link #missingSourceSymbols()}{@code .size()} <li>
     * {@code NUM_AVAILABLE_BYTES} = {@code 3} &times; {@link #availableRepairSymbols()}{@code .size()} </ul>
     * <p>
     * The write consists of the {@linkplain #sourceBlockNumber() source block number}, followed by a byte value
     * corresponding to the {@linkplain #state() latest state of the source block}, followed by
     * {@code NUM_MISSING_BYTES}, followed by the list of the encoding symbol identifiers (ESIs) of the
     * {@linkplain #missingSourceSymbols() missing source symbols}, followed by {@code NUM_AVAILABLE_BYTES}, followed by
     * the list of the ESIs of the {@linkplain #availableRepairSymbols() available repair symbols}.
     * <p>
     * The provided buffer must not be {@linkplain ByteBuffer#isReadOnly() read-only}, and must have at least
     * {@code (7 + NUM_MISSING_BYTES + NUM_AVAILABLE_BYTES)} bytes {@linkplain ByteBuffer#remaining() remaining}. If
     * this method returns normally, the position of the provided buffer will have been advanced by the same amount.
     * 
     * @param buffer
     *            A buffer on which the information is written
     * @exception ReadOnlyBufferException
     *                If the provided buffer is read-only
     * @exception BufferOverflowException
     *                If the provided buffer has less than {@code (7 + NUM_MISSING_BYTES + NUM_AVAILABLE_BYTES)} bytes
     *                remaining
     * @exception NullPointerException
     *                If the {@code buffer} is {@code null}
     */
    public void writeTo(ByteBuffer buffer) {

        Objects.requireNonNull(buffer);

        writeSBN(sbn, buffer);
        writeState(state, buffer);
        writeMissingSourceSymbols(missingSourceSymbols, buffer);
        writeAvailableRepairSymbols(availableRepairSymbols, buffer);
    }

    /**
     * Writes this information encoded in a compact format directly into the provided {@code DataOutput} object.
     * <p>
     * Let:
     * <ul>
     * <li>{@code NUM_MISSING_BYTES} = {@code 2} &times; {@link #missingSourceSymbols()}{@code .size()} <li>
     * {@code NUM_AVAILABLE_BYTES} = {@code 3} &times; {@link #availableRepairSymbols()}{@code .size()} </ul>
     * <p>
     * The method will write the {@linkplain #sourceBlockNumber() source block number}, followed by a byte value
     * corresponding to the {@linkplain #state() latest state of the source block}, followed by
     * {@code NUM_MISSING_BYTES}, followed by the list of the encoding symbol identifiers (ESIs) of the
     * {@linkplain #missingSourceSymbols() missing source symbols}, followed by {@code NUM_AVAILABLE_BYTES}, followed by
     * the list of the ESIs of the {@linkplain #availableRepairSymbols() available repair symbols}.
     * <p>
     * Examples of {@code DataOutput} objects are {@link java.io.DataOutputStream DataOutputStream} and
     * {@link java.io.ObjectOutputStream ObjectOutputStream}.
     * <p>
     * <b><em>Blocking behavior</em></b>: this method blocks until the whole information is written to the output, or an
     * {@code IOException} is throw.
     * 
     * @param out
     *            A {@code DataOutput} object into which the information is written
     * @throws IOException
     *             If an I/O error occurs while writing to the {@code DataOutput} object
     * @exception NullPointerException
     *                If {@code out} is {@code null}
     */
    public void writeTo(DataOutput out) throws IOException {

        Objects.requireNonNull(out);

        writeSBN(sbn, out);
        writeState(state, out);
        writeMissingSourceSymbols(missingSourceSymbols, out);
        writeAvailableRepairSymbols(availableRepairSymbols, out);
    }

    /**
     * Writes this information encoded in a compact format directly into the provided {@code WritableByteChannel}
     * object.
     * <p>
     * Let:
     * <ul>
     * <li>{@code NUM_MISSING_BYTES} = {@code 2} &times; {@link #missingSourceSymbols()}{@code .size()} <li>
     * {@code NUM_AVAILABLE_BYTES} = {@code 3} &times; {@link #availableRepairSymbols()}{@code .size()} </ul>
     * <p>
     * The method will write the {@linkplain #sourceBlockNumber() source block number}, followed by a byte value
     * corresponding to the {@linkplain #state() latest state of the source block}, followed by
     * {@code NUM_MISSING_BYTES}, followed by the list of the encoding symbol identifiers (ESIs) of the
     * {@linkplain #missingSourceSymbols() missing source symbols}, followed by {@code NUM_AVAILABLE_BYTES}, followed by
     * the list of the ESIs of the {@linkplain #availableRepairSymbols() available repair symbols}.
     * <p>
     * <b><em>Blocking behavior</em></b>: this method blocks until the whole information is written to the channel, or
     * an {@code IOException} is throw.
     * 
     * @param ch
     *            A {@code WritableByteChannel} object into which the information is written
     * @throws IOException
     *             If an I/O error occurs while writing to the {@code WritableByteChannel} object
     * @exception NullPointerException
     *                If {@code ch} is {@code null}
     */
    public void writeTo(WritableByteChannel ch) throws IOException {

        ExtraChannels.writeBytes(ch, asBuffer());
    }

    private int getEncodedByteSize() {

