/*
 * Copyright 2014-2019 Real Logic Ltd.
 *
 * 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 io.aeron.logbuffer;

import io.aeron.DirectBufferVector;
import io.aeron.ReservedValueSupplier;
import org.agrona.DirectBuffer;
import org.agrona.UnsafeAccess;
import org.agrona.concurrent.UnsafeBuffer;

import static io.aeron.logbuffer.FrameDescriptor.*;
import static io.aeron.logbuffer.LogBufferDescriptor.TERM_TAIL_COUNTERS_OFFSET;
import static io.aeron.logbuffer.LogBufferDescriptor.packTail;
import static io.aeron.protocol.DataHeaderFlyweight.HEADER_LENGTH;
import static io.aeron.protocol.DataHeaderFlyweight.RESERVED_VALUE_OFFSET;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static org.agrona.BitUtil.SIZE_OF_LONG;
import static org.agrona.BitUtil.align;

/**
 * Term buffer appender which supports a single exclusive producer writing an append-only log.
 *

 * Note: This class is NOT threadsafe.
 *

 * Messages are appended to a term using a framing protocol as described in {@link FrameDescriptor}.
 *

 * A default message header is applied to each message with the fields filled in for fragment flags, type, term number,
 * as appropriate.
 *

 * A message of type {@link FrameDescriptor#PADDING_FRAME_TYPE} is appended at the end of the buffer if claimed
 * space is not sufficiently large to accommodate the message about to be written.
 */
public final class ExclusiveTermAppender
{
    /**
     * The append operation tripped the end of the buffer and needs to rotate.
     */
    public static final int FAILED = -1;

    private final long tailAddressOffset;
    private final UnsafeBuffer termBuffer;

    /**
     * Construct a view over a term buffer and state buffer for appending frames.
     *
     * @param termBuffer     for where messages are stored.
     * @param metaDataBuffer for where the state of writers is stored manage concurrency.
     * @param partitionIndex for this will be the active appender.
     */
    public ExclusiveTermAppender(
        final UnsafeBuffer termBuffer, final UnsafeBuffer metaDataBuffer, final int partitionIndex)
    {
        final int tailCounterOffset = TERM_TAIL_COUNTERS_OFFSET + (partitionIndex * SIZE_OF_LONG);
        metaDataBuffer.boundsCheck(tailCounterOffset, SIZE_OF_LONG);

        this.termBuffer = termBuffer;
        tailAddressOffset = metaDataBuffer.addressOffset() + tailCounterOffset;
    }

    /**
     * Claim length of a the term buffer for writing in the message with zero copy semantics.
     *
     * @param termId      for the current term.
     * @param termOffset  in the term at which to append.
     * @param header      for writing the default header.
     * @param length      of the message to be written.
     * @param bufferClaim to be updated with the claimed region.
     * @return the resulting offset of the term after the append on success otherwise {@link #FAILED}.
     */
    public int claim(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final int length,
        final BufferClaim bufferClaim)
    {
        final int frameLength = length + HEADER_LENGTH;
        final int alignedLength = align(frameLength, FRAME_ALIGNMENT);
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + alignedLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            header.write(termBuffer, termOffset, frameLength, termId);
            bufferClaim.wrap(termBuffer, termOffset, frameLength);
        }

        return resultingOffset;
    }

    /**
     * Pad a length of the term buffer with a padding record.
     *
     * @param termId     for the current term.
     * @param termOffset in the term at which to append.
     * @param header     for writing the default header.
     * @param length     of the padding to be written.
     * @return the resulting offset of the term after success otherwise {@link #FAILED}.
     */
    public int appendPadding(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final int length)
    {
        final int frameLength = length + HEADER_LENGTH;
        final int alignedLength = align(frameLength, FRAME_ALIGNMENT);
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + alignedLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            header.write(termBuffer, termOffset, frameLength, termId);
            frameType(termBuffer, termOffset, PADDING_FRAME_TYPE);
            frameLengthOrdered(termBuffer, termOffset, frameLength);
        }

        return resultingOffset;
    }

    /**
     * Append an unfragmented message to the the term buffer.
     *
     * @param termId                for the current term.
     * @param termOffset            in the term at which to append.
     * @param header                for writing the default header.
     * @param srcBuffer             containing the message.
     * @param srcOffset             at which the message begins.
     * @param length                of the message in the source buffer.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return the resulting offset of the term after the append on success otherwise {@link #FAILED}.
     */
    public int appendUnfragmentedMessage(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final DirectBuffer srcBuffer,
        final int srcOffset,
        final int length,
        final ReservedValueSupplier reservedValueSupplier)
    {
        final int frameLength = length + HEADER_LENGTH;
        final int alignedLength = align(frameLength, FRAME_ALIGNMENT);
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + alignedLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            header.write(termBuffer, termOffset, frameLength, termId);
            termBuffer.putBytes(termOffset + HEADER_LENGTH, srcBuffer, srcOffset, length);

            if (null != reservedValueSupplier)
            {
                final long reservedValue = reservedValueSupplier.get(termBuffer, termOffset, frameLength);
                termBuffer.putLong(termOffset + RESERVED_VALUE_OFFSET, reservedValue, LITTLE_ENDIAN);
            }

            frameLengthOrdered(termBuffer, termOffset, frameLength);
        }

        return resultingOffset;
    }

    /**
     * Append an unfragmented message to the the term buffer.
     *
     * @param termId                for the current term.
     * @param termOffset            in the term at which to append.
     * @param header                for writing the default header.
     * @param bufferOne             containing the first part of the message.
     * @param offsetOne             at which the first part of the message begins.
     * @param lengthOne             of the first part of the message.
     * @param bufferTwo             containing the second part of the message.
     * @param offsetTwo             at which the second part of the message begins.
     * @param lengthTwo             of the second part of the message.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return the resulting offset of the term after the append on success otherwise {@link #FAILED}.
     */
    public int appendUnfragmentedMessage(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final DirectBuffer bufferOne,
        final int offsetOne,
        final int lengthOne,
        final DirectBuffer bufferTwo,
        final int offsetTwo,
        final int lengthTwo,
        final ReservedValueSupplier reservedValueSupplier)
    {
        final int frameLength = lengthOne + lengthTwo + HEADER_LENGTH;
        final int alignedLength = align(frameLength, FRAME_ALIGNMENT);
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + alignedLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            header.write(termBuffer, termOffset, frameLength, termId);
            termBuffer.putBytes(termOffset + HEADER_LENGTH, bufferOne, offsetOne, lengthOne);
            termBuffer.putBytes(termOffset + HEADER_LENGTH + lengthOne, bufferTwo, offsetTwo, lengthTwo);

            if (null != reservedValueSupplier)
            {
                final long reservedValue = reservedValueSupplier.get(termBuffer, termOffset, frameLength);
                termBuffer.putLong(termOffset + RESERVED_VALUE_OFFSET, reservedValue, LITTLE_ENDIAN);
            }

            frameLengthOrdered(termBuffer, termOffset, frameLength);
        }

        return resultingOffset;
    }

    /**
     * Append an unfragmented message to the the term buffer as a gathering of vectors.
     *
     * @param termId                for the current term.
     * @param termOffset            in the term at which to append.
     * @param header                for writing the default header.
     * @param vectors               to the buffers.
     * @param length                of the message as a sum of the vectors.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return the resulting offset of the term after the append on success otherwise {@link #FAILED}.
     */
    public int appendUnfragmentedMessage(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final DirectBufferVector[] vectors,
        final int length,
        final ReservedValueSupplier reservedValueSupplier)
    {
        final int frameLength = length + HEADER_LENGTH;
        final int alignedLength = align(frameLength, FRAME_ALIGNMENT);
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + alignedLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            header.write(termBuffer, termOffset, frameLength, termId);

            int offset = termOffset + HEADER_LENGTH;
            for (final DirectBufferVector vector : vectors)
            {
                termBuffer.putBytes(offset, vector.buffer, vector.offset, vector.length);
                offset += vector.length;
            }

            if (null != reservedValueSupplier)
            {
                final long reservedValue = reservedValueSupplier.get(termBuffer, termOffset, frameLength);
                termBuffer.putLong(termOffset + RESERVED_VALUE_OFFSET, reservedValue, LITTLE_ENDIAN);
            }

            frameLengthOrdered(termBuffer, termOffset, frameLength);
        }

        return resultingOffset;
    }

    /**
     * Append a fragmented message to the the term buffer.
     * The message will be split up into fragments of MTU length minus header.
     *
     * @param termId                for the current term.
     * @param termOffset            in the term at which to append.
     * @param header                for writing the default header.
     * @param srcBuffer             containing the message.
     * @param srcOffset             at which the message begins.
     * @param length                of the message in the source buffer.
     * @param maxPayloadLength      that the message will be fragmented into.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return the resulting offset of the term after the append on success otherwise {@link #FAILED}.
     */
    public int appendFragmentedMessage(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final DirectBuffer srcBuffer,
        final int srcOffset,
        final int length,
        final int maxPayloadLength,
        final ReservedValueSupplier reservedValueSupplier)
    {
        final int numMaxPayloads = length / maxPayloadLength;
        final int remainingPayload = length % maxPayloadLength;
        final int lastFrameLength = remainingPayload > 0 ? align(remainingPayload + HEADER_LENGTH, FRAME_ALIGNMENT) : 0;
        final int requiredLength = (numMaxPayloads * (maxPayloadLength + HEADER_LENGTH)) + lastFrameLength;
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + requiredLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            int frameOffset = termOffset;
            byte flags = BEGIN_FRAG_FLAG;
            int remaining = length;
            do
            {
                final int bytesToWrite = Math.min(remaining, maxPayloadLength);
                final int frameLength = bytesToWrite + HEADER_LENGTH;
                final int alignedLength = align(frameLength, FRAME_ALIGNMENT);

                header.write(termBuffer, frameOffset, frameLength, termId);
                termBuffer.putBytes(
                    frameOffset + HEADER_LENGTH,
                    srcBuffer,
                    srcOffset + (length - remaining),
                    bytesToWrite);

                if (remaining <= maxPayloadLength)
                {
                    flags |= END_FRAG_FLAG;
                }

                frameFlags(termBuffer, frameOffset, flags);

                if (null != reservedValueSupplier)
                {
                    final long reservedValue = reservedValueSupplier.get(termBuffer, frameOffset, frameLength);
                    termBuffer.putLong(frameOffset + RESERVED_VALUE_OFFSET, reservedValue, LITTLE_ENDIAN);
                }

                frameLengthOrdered(termBuffer, frameOffset, frameLength);

                flags = 0;
                frameOffset += alignedLength;
                remaining -= bytesToWrite;
            }
            while (remaining > 0);
        }

        return resultingOffset;
    }

    /**
     * Append a fragmented message to the the term buffer.
     * The message will be split up into fragments of MTU length minus header.
     *
     * @param termId                for the current term.
     * @param termOffset            in the term at which to append.
     * @param header                for writing the default header.
     * @param bufferOne             containing the first part of the message.
     * @param offsetOne             at which the first part of the message begins.
     * @param lengthOne             of the first part of the message.
     * @param bufferTwo             containing the second part of the message.
     * @param offsetTwo             at which the second part of the message begins.
     * @param lengthTwo             of the second part of the message.
     * @param maxPayloadLength      that the message will be fragmented into.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return the resulting offset of the term after the append on success otherwise {@link #FAILED}.
     */
    public int appendFragmentedMessage(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final DirectBuffer bufferOne,
        final int offsetOne,
        final int lengthOne,
        final DirectBuffer bufferTwo,
        final int offsetTwo,
        final int lengthTwo,
        final int maxPayloadLength,
        final ReservedValueSupplier reservedValueSupplier)
    {
        final int length = lengthOne + lengthTwo;
        final int numMaxPayloads = length / maxPayloadLength;
        final int remainingPayload = length % maxPayloadLength;
        final int lastFrameLength = remainingPayload > 0 ? align(remainingPayload + HEADER_LENGTH, FRAME_ALIGNMENT) : 0;
        final int requiredLength = (numMaxPayloads * (maxPayloadLength + HEADER_LENGTH)) + lastFrameLength;
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + requiredLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            int frameOffset = termOffset;
            byte flags = BEGIN_FRAG_FLAG;
            int remaining = length;
            int positionOne = 0;
            int positionTwo = 0;

            do
            {
                final int bytesToWrite = Math.min(remaining, maxPayloadLength);
                final int frameLength = bytesToWrite + HEADER_LENGTH;
                final int alignedLength = align(frameLength, FRAME_ALIGNMENT);

                header.write(termBuffer, frameOffset, frameLength, termId);

                int bytesWritten = 0;
                int payloadOffset = frameOffset + HEADER_LENGTH;
                do
                {
                    final int remainingOne = lengthOne - positionOne;
                    if (remainingOne > 0)
                    {
                        final int numBytes = Math.min(bytesToWrite - bytesWritten, remainingOne);
                        termBuffer.putBytes(payloadOffset, bufferOne, offsetOne + positionOne, numBytes);

                        bytesWritten += numBytes;
                        payloadOffset += numBytes;
                        positionOne += numBytes;
                    }
                    else
                    {
                        final int numBytes = Math.min(bytesToWrite - bytesWritten, lengthTwo - positionTwo);
                        termBuffer.putBytes(payloadOffset, bufferTwo, offsetTwo + positionTwo, numBytes);

                        bytesWritten += numBytes;
                        payloadOffset += numBytes;
                        positionTwo += numBytes;
                    }
                }
                while (bytesWritten < bytesToWrite);

                if (remaining <= maxPayloadLength)
                {
                    flags |= END_FRAG_FLAG;
                }

                frameFlags(termBuffer, frameOffset, flags);

                if (null != reservedValueSupplier)
                {
                    final long reservedValue = reservedValueSupplier.get(termBuffer, frameOffset, frameLength);
                    termBuffer.putLong(frameOffset + RESERVED_VALUE_OFFSET, reservedValue, LITTLE_ENDIAN);
                }

                frameLengthOrdered(termBuffer, frameOffset, frameLength);

                flags = 0;
                frameOffset += alignedLength;
                remaining -= bytesToWrite;
            }
            while (remaining > 0);
        }

        return resultingOffset;
    }

    /**
     * Append a fragmented message to the the term buffer.
     * The message will be split up into fragments of MTU length minus header.
     *
     * @param termId                for the current term.
     * @param termOffset            in the term at which to append.
     * @param header                for writing the default header.
     * @param vectors               to the buffers.
     * @param length                of the message in the source buffer.
     * @param maxPayloadLength      that the message will be fragmented into.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return the resulting offset of the term after the append on success otherwise {@link #FAILED}.
     */
    public int appendFragmentedMessage(
        final int termId,
        final int termOffset,
        final HeaderWriter header,
        final DirectBufferVector[] vectors,
        final int length,
        final int maxPayloadLength,
        final ReservedValueSupplier reservedValueSupplier)
    {
        final int numMaxPayloads = length / maxPayloadLength;
        final int remainingPayload = length % maxPayloadLength;
        final int lastFrameLength = remainingPayload > 0 ? align(remainingPayload + HEADER_LENGTH, FRAME_ALIGNMENT) : 0;
        final int requiredLength = (numMaxPayloads * (maxPayloadLength + HEADER_LENGTH)) + lastFrameLength;
        final UnsafeBuffer termBuffer = this.termBuffer;
        final int termLength = termBuffer.capacity();

        int resultingOffset = termOffset + requiredLength;
        putRawTailOrdered(termId, resultingOffset);

        if (resultingOffset > termLength)
        {
            resultingOffset = handleEndOfLogCondition(termBuffer, termOffset, header, termLength, termId);
        }
        else
        {
            int frameOffset = termOffset;
            byte flags = BEGIN_FRAG_FLAG;
            int remaining = length;
            int vectorIndex = 0;
            int vectorOffset = 0;

            do
            {
                final int bytesToWrite = Math.min(remaining, maxPayloadLength);
                final int frameLength = bytesToWrite + HEADER_LENGTH;
                final int alignedLength = align(frameLength, FRAME_ALIGNMENT);

                header.write(termBuffer, frameOffset, frameLength, termId);

                int bytesWritten = 0;
                int payloadOffset = frameOffset + HEADER_LENGTH;
                do
                {
                    final DirectBufferVector vector = vectors[vectorIndex];
                    final int vectorRemaining = vector.length - vectorOffset;
                    final int numBytes = Math.min(bytesToWrite - bytesWritten, vectorRemaining);

                    termBuffer.putBytes(payloadOffset, vector.buffer, vector.offset + vectorOffset, numBytes);

                    bytesWritten += numBytes;
                    payloadOffset += numBytes;
                    vectorOffset += numBytes;

                    if (vectorRemaining <= numBytes)
                    {
                        vectorIndex++;
                        vectorOffset = 0;
                    }
                }
                while (bytesWritten < bytesToWrite);

                if (remaining <= maxPayloadLength)
                {
                    flags |= END_FRAG_FLAG;
                }

                frameFlags(termBuffer, frameOffset, flags);

                if (null != reservedValueSupplier)
                {
                    final long reservedValue = reservedValueSupplier.get(termBuffer, frameOffset, frameLength);
                    termBuffer.putLong(frameOffset + RESERVED_VALUE_OFFSET, reservedValue, LITTLE_ENDIAN);
                }

                frameLengthOrdered(termBuffer, frameOffset, frameLength);

                flags = 0;
                frameOffset += alignedLength;
                remaining -= bytesToWrite;
            }
            while (remaining > 0);
        }

        return resultingOffset;
    }

    private static int handleEndOfLogCondition(
        final UnsafeBuffer termBuffer,
        final long termOffset,
        final HeaderWriter header,
        final int termLength,
        final int termId)
    {
        if (termOffset < termLength)
        {
            final int offset = (int)termOffset;
            final int paddingLength = termLength - offset;
            header.write(termBuffer, offset, paddingLength, termId);
            frameType(termBuffer, offset, PADDING_FRAME_TYPE);
            frameLengthOrdered(termBuffer, offset, paddingLength);
        }

        return FAILED;
    }

    private void putRawTailOrdered(final int termId, final int termOffset)
    {
        UnsafeAccess.UNSAFE.putOrderedLong(null, tailAddressOffset, packTail(termId, termOffset));
    }
}


/*
 * Copyright 2014-2019 Real Logic Ltd.
 *
 * 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 io.aeron.driver;

import io.aeron.CommonContext;
import io.aeron.driver.buffer.RawLog;
import io.aeron.driver.media.SendChannelEndpoint;
import io.aeron.driver.status.SystemCounters;
import io.aeron.logbuffer.LogBufferDescriptor;
import io.aeron.logbuffer.LogBufferUnblocker;
import io.aeron.protocol.DataHeaderFlyweight;
import io.aeron.protocol.RttMeasurementFlyweight;
import io.aeron.protocol.SetupFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import org.agrona.collections.ArrayListUtil;
import org.agrona.collections.ArrayUtil;
import org.agrona.concurrent.NanoClock;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.status.AtomicCounter;
import org.agrona.concurrent.status.Position;
import org.agrona.concurrent.status.ReadablePosition;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;

import static io.aeron.driver.Configuration.PUBLICATION_HEARTBEAT_TIMEOUT_NS;
import static io.aeron.driver.Configuration.PUBLICATION_SETUP_TIMEOUT_NS;
import static io.aeron.driver.status.SystemCounterDescriptor.*;
import static io.aeron.logbuffer.LogBufferDescriptor.*;
import static io.aeron.logbuffer.TermScanner.*;
import static io.aeron.protocol.DataHeaderFlyweight.BEGIN_AND_END_FLAGS;
import static io.aeron.protocol.DataHeaderFlyweight.BEGIN_END_AND_EOS_FLAGS;

class NetworkPublicationPadding1
{
    @SuppressWarnings("unused")
    protected long p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15;
}

class NetworkPublicationConductorFields extends NetworkPublicationPadding1
{
    protected static final ReadablePosition[] EMPTY_POSITIONS = new ReadablePosition[0];

    protected long cleanPosition = 0;
    protected long timeOfLastActivityNs = 0;
    protected long lastSenderPosition = 0;
    protected int refCount = 0;
    protected ReadablePosition[] spyPositions = EMPTY_POSITIONS;
    protected final ArrayList<UntetheredSubscription> untetheredSubscriptions = new ArrayList<>();
}

class NetworkPublicationPadding2 extends NetworkPublicationConductorFields
{
    @SuppressWarnings("unused")
    protected long p16, p17, p18, p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30;
}

class NetworkPublicationSenderFields extends NetworkPublicationPadding2
{
    protected long timeOfLastSendOrHeartbeatNs;
    protected long timeOfLastSetupNs;
    protected long statusMessageDeadlineNs;
    protected boolean trackSenderLimits = true;
    protected boolean shouldSendSetupFrame = true;
}

class NetworkPublicationPadding3 extends NetworkPublicationSenderFields
{
    @SuppressWarnings("unused")
    protected long p31, p32, p33, p34, p35, p36, p37, p38, p39, p40, p41, p42, p43, p44, p45;
}

/**
 * Publication to be sent to connected subscribers.
 */
public class NetworkPublication
    extends NetworkPublicationPadding3
    implements RetransmitSender, DriverManagedResource, Subscribable
{
    public enum State
    {
        ACTIVE, DRAINING, LINGER, CLOSING
    }

    private final long registrationId;
    private final long unblockTimeoutNs;
    private final long connectionTimeoutNs;
    private final long lingerTimeoutNs;
    private final long untetheredWindowLimitTimeoutNs;
    private final long untetheredRestingTimeoutNs;
    private final long tag;
    private final int positionBitsToShift;
    private final int initialTermId;
    private final int termBufferLength;
    private final int termLengthMask;
    private final int mtuLength;
    private final int termWindowLength;
    private final int sessionId;
    private final int streamId;
    private final boolean isExclusive;
    private final boolean spiesSimulateConnection;
    private final boolean signalEos;
    private volatile boolean hasReceivers;
    private volatile boolean hasSpies;
    private volatile boolean isConnected;
    private volatile boolean isEndOfStream;
    private volatile boolean hasSenderReleased;
    private State state = State.ACTIVE;

    private final UnsafeBuffer[] termBuffers;
    private final ByteBuffer[] sendBuffers;
    private final Position publisherPos;
    private final Position publisherLimit;
    private final Position senderPosition;
    private final Position senderLimit;
    private final SendChannelEndpoint channelEndpoint;
    private final ByteBuffer heartbeatBuffer;
    private final DataHeaderFlyweight heartbeatDataHeader;
    private final ByteBuffer setupBuffer;
    private final SetupFlyweight setupHeader;
    private final ByteBuffer rttMeasurementBuffer;
    private final RttMeasurementFlyweight rttMeasurementHeader;
    private final FlowControl flowControl;
    private final NanoClock nanoClock;
    private final RetransmitHandler retransmitHandler;
    private final UnsafeBuffer metaDataBuffer;
    private final RawLog rawLog;
    private final AtomicCounter heartbeatsSent;
    private final AtomicCounter retransmitsSent;
    private final AtomicCounter senderFlowControlLimits;
    private final AtomicCounter senderBpe;
    private final AtomicCounter shortSends;
    private final AtomicCounter unblockedPublications;

    public NetworkPublication(
        final long registrationId,
        final long tag,
        final SendChannelEndpoint channelEndpoint,
        final NanoClock nanoClock,
        final RawLog rawLog,
        final int termWindowLength,
        final Position publisherPos,
        final Position publisherLimit,
        final Position senderPosition,
        final Position senderLimit,
        final AtomicCounter senderBpe,
        final int sessionId,
        final int streamId,
        final int initialTermId,
        final int mtuLength,
        final SystemCounters systemCounters,
        final FlowControl flowControl,
        final RetransmitHandler retransmitHandler,
        final NetworkPublicationThreadLocals threadLocals,
        final long unblockTimeoutNs,
        final long connectionTimeoutNs,
        final long lingerTimeoutNs,
        final long untetheredWindowLimitTimeoutNs,
        final long untetheredRestingTimeoutNs,
        final boolean isExclusive,
        final boolean spiesSimulateConnection,
        final boolean signalEos)
    {
        this.registrationId = registrationId;
        this.unblockTimeoutNs = unblockTimeoutNs;
        this.connectionTimeoutNs = connectionTimeoutNs;
        this.lingerTimeoutNs = lingerTimeoutNs;
        this.untetheredWindowLimitTimeoutNs = untetheredWindowLimitTimeoutNs;
        this.untetheredRestingTimeoutNs = untetheredRestingTimeoutNs;
        this.tag = tag;
        this.channelEndpoint = channelEndpoint;
        this.rawLog = rawLog;
        this.nanoClock = nanoClock;
        this.senderPosition = senderPosition;
        this.senderLimit = senderLimit;
        this.flowControl = flowControl;
        this.retransmitHandler = retransmitHandler;
        this.publisherPos = publisherPos;
        this.publisherLimit = publisherLimit;
        this.mtuLength = mtuLength;
        this.initialTermId = initialTermId;
        this.sessionId = sessionId;
        this.streamId = streamId;
        this.isExclusive = isExclusive;
        this.spiesSimulateConnection = spiesSimulateConnection;
        this.signalEos = signalEos;

        metaDataBuffer = rawLog.metaData();
        setupBuffer = threadLocals.setupBuffer();
        setupHeader = threadLocals.setupHeader();
        heartbeatBuffer = threadLocals.heartbeatBuffer();
        heartbeatDataHeader = threadLocals.heartbeatDataHeader();
        rttMeasurementBuffer = threadLocals.rttMeasurementBuffer();
        rttMeasurementHeader = threadLocals.rttMeasurementHeader();

        heartbeatsSent = systemCounters.get(HEARTBEATS_SENT);
        shortSends = systemCounters.get(SHORT_SENDS);
        retransmitsSent = systemCounters.get(RETRANSMITS_SENT);
        senderFlowControlLimits = systemCounters.get(SENDER_FLOW_CONTROL_LIMITS);
        unblockedPublications = systemCounters.get(UNBLOCKED_PUBLICATIONS);
        this.senderBpe = senderBpe;

        termBuffers = rawLog.termBuffers();
        sendBuffers = rawLog.sliceTerms();

        final int termLength = rawLog.termLength();
        termBufferLength = termLength;
        termLengthMask = termLength - 1;
        flowControl.initialize(initialTermId, termLength);

        final long nowNs = nanoClock.nanoTime();
        timeOfLastSendOrHeartbeatNs = nowNs - PUBLICATION_HEARTBEAT_TIMEOUT_NS - 1;
        timeOfLastSetupNs = nowNs - PUBLICATION_SETUP_TIMEOUT_NS - 1;
        statusMessageDeadlineNs = spiesSimulateConnection ? nowNs : (nowNs + connectionTimeoutNs);

        positionBitsToShift = LogBufferDescriptor.positionBitsToShift(termLength);
        this.termWindowLength = termWindowLength;

        lastSenderPosition = senderPosition.get();
        cleanPosition = lastSenderPosition;
        timeOfLastActivityNs = nowNs;
    }

    public boolean free()
    {
        return rawLog.free();
    }

    public void close()
    {
        publisherPos.close();
        publisherLimit.close();
        senderPosition.close();
        senderLimit.close();
        for (final ReadablePosition position : spyPositions)
        {
            position.close();
        }

        for (int i = 0, size = untetheredSubscriptions.size(); i < size; i++)
        {
            final UntetheredSubscription untetheredSubscription = untetheredSubscriptions.get(i);
            if (UntetheredSubscription.RESTING == untetheredSubscription.state)
            {
                untetheredSubscription.position.close();
            }
        }

        rawLog.close();
    }

    public long tag()
    {
        return tag;
    }

    public int termBufferLength()
    {
        return termBufferLength;
    }

    public int mtuLength()
    {
        return mtuLength;
    }

    public long registrationId()
    {
        return registrationId;
    }

    public boolean isExclusive()
    {
        return isExclusive;
    }

    public final int send(final long nowNs)
    {
        final long senderPosition = this.senderPosition.get();
        final int activeTermId = computeTermIdFromPosition(senderPosition, positionBitsToShift, initialTermId);
        final int termOffset = (int)senderPosition & termLengthMask;

        if (shouldSendSetupFrame)
        {
            setupMessageCheck(nowNs, activeTermId, termOffset);
        }

        int bytesSent = sendData(nowNs, senderPosition, termOffset);

        if (0 == bytesSent)
        {
            final boolean isEndOfStream = this.isEndOfStream;
            bytesSent = heartbeatMessageCheck(nowNs, activeTermId, termOffset, signalEos && isEndOfStream);

            if (spiesSimulateConnection && (statusMessageDeadlineNs - nowNs < 0) && hasSpies)
            {
                final long newSenderPosition = maxSpyPosition(senderPosition);
                this.senderPosition.setOrdered(newSenderPosition);
                senderLimit.setOrdered(flowControl.onIdle(nowNs, newSenderPosition, newSenderPosition, isEndOfStream));
            }
            else
            {
                senderLimit.setOrdered(flowControl.onIdle(nowNs, senderLimit.get(), senderPosition, isEndOfStream));
            }
        }

        updateHasReceivers(nowNs);
        retransmitHandler.processTimeouts(nowNs, this);

        return bytesSent;
    }

    public SendChannelEndpoint channelEndpoint()
    {
        return channelEndpoint;
    }

    public String channel()
    {
        return channelEndpoint.originalUriString();
    }

    public int sessionId()
    {
        return sessionId;
    }

    public int streamId()
    {
        return streamId;
    }

    public void resend(final int termId, final int termOffset, final int length)
    {
        final long senderPosition = this.senderPosition.get();
        final long resendPosition = computePosition(termId, termOffset, positionBitsToShift, initialTermId);

        if (resendPosition < senderPosition && resendPosition >= (senderPosition - termBufferLength))
        {
            final int activeIndex = indexByPosition(resendPosition, positionBitsToShift);
            final UnsafeBuffer termBuffer = termBuffers[activeIndex];
            final ByteBuffer sendBuffer = sendBuffers[activeIndex];

            int remainingBytes = length;
            int bytesSent = 0;
            int offset = termOffset;
            do
            {
                offset += bytesSent;

                final long scanOutcome = scanForAvailability(termBuffer, offset, Math.min(mtuLength, remainingBytes));
                final int available = available(scanOutcome);
                if (available <= 0)
                {
                    break;
                }

                sendBuffer.limit(offset + available).position(offset);

                if (available != channelEndpoint.send(sendBuffer))
                {
                    shortSends.increment();
                    break;
                }

                bytesSent = available + padding(scanOutcome);
                remainingBytes -= bytesSent;
            }
            while (remainingBytes > 0);

            retransmitsSent.incrementOrdered();
        }
    }

    public void triggerSendSetupFrame()
    {
        if (!isEndOfStream)
        {
            shouldSendSetupFrame = true;
        }
    }

    public void addSubscriber(final SubscriptionLink subscriptionLink, final ReadablePosition position)
    {
        spyPositions = ArrayUtil.add(spyPositions, position);
        hasSpies = true;

        if (!subscriptionLink.isTether())
        {
            untetheredSubscriptions.add(new UntetheredSubscription(subscriptionLink, position, nanoClock.nanoTime()));
        }

        if (spiesSimulateConnection)
        {
            LogBufferDescriptor.isConnected(metaDataBuffer, true);
            isConnected = true;
        }
    }

    public void removeSubscriber(final SubscriptionLink subscriptionLink, final ReadablePosition position)
    {
        spyPositions = ArrayUtil.remove(spyPositions, position);
        hasSpies = spyPositions.length > 0;
        position.close();

        if (!subscriptionLink.isTether())
        {
            for (int lastIndex = untetheredSubscriptions.size() - 1, i = lastIndex; i >= 0; i--)
            {
                if (untetheredSubscriptions.get(i).subscriptionLink == subscriptionLink)
                {
                    ArrayListUtil.fastUnorderedRemove(untetheredSubscriptions, i, lastIndex);
                    break;
                }
            }
        }
    }

    public void onNak(final int termId, final int termOffset, final int length)
    {
        retransmitHandler.onNak(termId, termOffset, length, termBufferLength, this);
    }

    public void onStatusMessage(final StatusMessageFlyweight msg, final InetSocketAddress srcAddress)
    {
        if (!hasReceivers)
        {
            hasReceivers = true;
        }

        final long timeNs = nanoClock.nanoTime();
        statusMessageDeadlineNs = timeNs + connectionTimeoutNs;

        final long limit = flowControl.onStatusMessage(
            msg,
            srcAddress,
            senderLimit.get(),
            initialTermId,
            positionBitsToShift,
            timeNs);

        senderLimit.setOrdered(limit);

        if (!isConnected)
        {
            LogBufferDescriptor.isConnected(metaDataBuffer, true);
            isConnected = true;
        }
    }

    public void onRttMeasurement(
        final RttMeasurementFlyweight msg, @SuppressWarnings("unused") final InetSocketAddress srcAddress)
    {
        if (RttMeasurementFlyweight.REPLY_FLAG == (msg.flags() & RttMeasurementFlyweight.REPLY_FLAG))
        {
            rttMeasurementHeader
                .receiverId(msg.receiverId())
                .echoTimestampNs(msg.echoTimestampNs())
                .receptionDelta(0)
                .sessionId(sessionId)
                .streamId(streamId)
                .flags((short)0x0);

            final int bytesSent = channelEndpoint.send(rttMeasurementBuffer);
            if (RttMeasurementFlyweight.HEADER_LENGTH != bytesSent)
            {
                shortSends.increment();
            }
        }

        // handling of RTT measurements would be done in an else clause here.
    }

    RawLog rawLog()
    {
        return rawLog;
    }

    int publisherLimitId()
    {
        return publisherLimit.id();
    }

    /**
     * Update the publishers limit for flow control as part of the conductor duty cycle.
     *
     * @return 1 if the limit has been updated otherwise 0.
     */
    final int updatePublisherLimit()
    {
        int workCount = 0;

        final long senderPosition = this.senderPosition.getVolatile();
        if (hasReceivers || (spiesSimulateConnection && spyPositions.length > 0))
        {
            long minConsumerPosition = senderPosition;
            for (final ReadablePosition spyPosition : spyPositions)
            {
                minConsumerPosition = Math.min(minConsumerPosition, spyPosition.getVolatile());
            }

            final long proposedPublisherLimit = minConsumerPosition + termWindowLength;
            if (publisherLimit.proposeMaxOrdered(proposedPublisherLimit))
            {
                cleanBuffer(proposedPublisherLimit);
                workCount = 1;
            }
        }
        else if (publisherLimit.get() > senderPosition)
        {
            publisherLimit.setOrdered(senderPosition);
        }

        return workCount;
    }

    boolean hasSpies()
    {
        return hasSpies;
    }

    final void updateHasReceivers(final long timeNs)
    {
        if ((statusMessageDeadlineNs - timeNs < 0) && hasReceivers)
        {
            hasReceivers = false;
        }
    }

    private int sendData(final long nowNs, final long senderPosition, final int termOffset)
    {
        int bytesSent = 0;
        final int availableWindow = (int)(senderLimit.get() - senderPosition);
        if (availableWindow > 0)
        {
            final int scanLimit = Math.min(availableWindow, mtuLength);
            final int activeIndex = indexByPosition(senderPosition, positionBitsToShift);

            final long scanOutcome = scanForAvailability(termBuffers[activeIndex], termOffset, scanLimit);
            final int available = available(scanOutcome);
            if (available > 0)
            {
                final ByteBuffer sendBuffer = sendBuffers[activeIndex];
                sendBuffer.limit(termOffset + available).position(termOffset);

                if (available == channelEndpoint.send(sendBuffer))
                {
                    timeOfLastSendOrHeartbeatNs = nowNs;
                    trackSenderLimits = true;

                    bytesSent = available;
                    this.senderPosition.setOrdered(senderPosition + bytesSent + padding(scanOutcome));
                }
                else
                {
                    shortSends.increment();
                }
            }
        }
        else if (trackSenderLimits)
        {
            trackSenderLimits = false;
            senderBpe.incrementOrdered();
            senderFlowControlLimits.incrementOrdered();
        }

        return bytesSent;
    }

    private void setupMessageCheck(final long nowNs, final int activeTermId, final int termOffset)
    {
        if ((timeOfLastSetupNs + PUBLICATION_SETUP_TIMEOUT_NS) - nowNs < 0)
        {
            timeOfLastSetupNs = nowNs;
            timeOfLastSendOrHeartbeatNs = nowNs;

            setupBuffer.clear();
            setupHeader
                .activeTermId(activeTermId)
                .termOffset(termOffset)
                .sessionId(sessionId)
                .streamId(streamId)
                .initialTermId(initialTermId)
                .termLength(termBufferLength)
                .mtuLength(mtuLength)
                .ttl(channelEndpoint.multicastTtl());

            if (SetupFlyweight.HEADER_LENGTH != channelEndpoint.send(setupBuffer))
            {
                shortSends.increment();
            }

            if (hasReceivers)
            {
                shouldSendSetupFrame = false;
            }
        }
    }

    private int heartbeatMessageCheck(
        final long nowNs, final int activeTermId, final int termOffset, final boolean signalEos)
    {
        int bytesSent = 0;

        if ((timeOfLastSendOrHeartbeatNs + PUBLICATION_HEARTBEAT_TIMEOUT_NS) - nowNs < 0)
        {
            heartbeatBuffer.clear();
            heartbeatDataHeader
                .sessionId(sessionId)
                .streamId(streamId)
                .termId(activeTermId)
                .termOffset(termOffset)
                .flags((byte)(signalEos ? BEGIN_END_AND_EOS_FLAGS : BEGIN_AND_END_FLAGS));

            bytesSent = channelEndpoint.send(heartbeatBuffer);
            if (DataHeaderFlyweight.HEADER_LENGTH != bytesSent)
            {
                shortSends.increment();
            }

            timeOfLastSendOrHeartbeatNs = nowNs;
            heartbeatsSent.incrementOrdered();
        }

        return bytesSent;
    }

    private void cleanBuffer(final long publisherLimit)
    {
        final long cleanPosition = this.cleanPosition;
        final long dirtyRange = publisherLimit - cleanPosition;
        final int bufferCapacity = termBufferLength;
        final int reservedRange = bufferCapacity * 2;

        if (dirtyRange > reservedRange)
        {
            final UnsafeBuffer dirtyTerm = termBuffers[indexByPosition(cleanPosition, positionBitsToShift)];
            final int termOffset = (int)cleanPosition & termLengthMask;
            final int bytesForCleaning = (int)(dirtyRange - reservedRange);
            final int length = Math.min(bytesForCleaning, bufferCapacity - termOffset);

            dirtyTerm.setMemory(termOffset, length, (byte)0);
            this.cleanPosition = cleanPosition + length;
        }
    }

    private void checkForBlockedPublisher(final long producerPosition, final long senderPosition, final long nowNs)
    {
        if (senderPosition == lastSenderPosition && isPossiblyBlocked(producerPosition, senderPosition))
        {
            if ((timeOfLastActivityNs + unblockTimeoutNs) - nowNs < 0)
            {
                if (LogBufferUnblocker.unblock(termBuffers, metaDataBuffer, senderPosition, termBufferLength))
                {
                    unblockedPublications.incrementOrdered();
                }
            }
        }
        else
        {
            timeOfLastActivityNs = nowNs;
            lastSenderPosition = senderPosition;
        }
    }

    private boolean isPossiblyBlocked(final long producerPosition, final long consumerPosition)
    {
        final int producerTermCount = activeTermCount(metaDataBuffer);
        final int expectedTermCount = (int)(consumerPosition >> positionBitsToShift);

        if (producerTermCount != expectedTermCount)
        {
            return true;
        }

        return producerPosition > consumerPosition;
    }

    private boolean spiesFinishedConsuming(final DriverConductor conductor, final long eosPosition)
    {
        if (spyPositions.length > 0)
        {
            for (final ReadablePosition spyPosition : spyPositions)
            {
                if (spyPosition.getVolatile() < eosPosition)
                {
                    return false;
                }
            }

            hasSpies = false;
            conductor.cleanupSpies(this);
        }

        return true;
    }

    private long maxSpyPosition(final long senderPosition)
    {
        long position = senderPosition;

        for (final ReadablePosition spyPosition : spyPositions)
        {
            position = Math.max(position, spyPosition.getVolatile());
        }

        return position;
    }

    private void updateConnectedStatus()
    {
        final boolean currentConnectedState = hasReceivers || (spiesSimulateConnection && spyPositions.length > 0);

        if (currentConnectedState != isConnected)
        {
            LogBufferDescriptor.isConnected(metaDataBuffer, currentConnectedState);
            isConnected = currentConnectedState;
        }
    }

    private void checkUntetheredSubscriptions(final long nowNs, final DriverConductor conductor)
    {
        final ArrayList<UntetheredSubscription> untetheredSubscriptions = this.untetheredSubscriptions;
        final int untetheredSubscriptionsSize = untetheredSubscriptions.size();
        if (0 == untetheredSubscriptionsSize)
        {
            return;
        }

        final long senderPosition = this.senderPosition.getVolatile();
        final long untetheredWindowLimit = (senderPosition - termWindowLength) + (termWindowLength >> 4);

        for (int lastIndex = untetheredSubscriptionsSize - 1, i = lastIndex; i >= 0; i--)
        {
            final UntetheredSubscription untethered = untetheredSubscriptions.get(i);
            switch (untethered.state)
            {
                case UntetheredSubscription.ACTIVE:
                    if (untethered.position.getVolatile() > untetheredWindowLimit)
                    {
                        untethered.timeOfLastUpdateNs = nowNs;
                    }
                    else if ((untethered.timeOfLastUpdateNs + untetheredWindowLimitTimeoutNs) - nowNs <= 0)
                    {
                        conductor.notifyUnavailableImageLink(registrationId, untethered.subscriptionLink);
                        untethered.state = UntetheredSubscription.LINGER;
                        untethered.timeOfLastUpdateNs = nowNs;
                    }
                    break;

                case UntetheredSubscription.LINGER:
                    if ((untethered.timeOfLastUpdateNs + untetheredWindowLimitTimeoutNs) - nowNs <= 0)
                    {
                        spyPositions = ArrayUtil.remove(spyPositions, untethered.position);
                        untethered.state = UntetheredSubscription.RESTING;
                        untethered.timeOfLastUpdateNs = nowNs;
                    }
                    break;

                case UntetheredSubscription.RESTING:
                    if ((untethered.timeOfLastUpdateNs + untetheredRestingTimeoutNs) - nowNs <= 0)
                    {
                        spyPositions = ArrayUtil.add(spyPositions, untethered.position);
                        conductor.notifyAvailableImageLink(
                            registrationId,
                            sessionId,
                            untethered.subscriptionLink,
                            untethered.position.id(),
                            senderPosition,
                            rawLog.fileName(),
                            CommonContext.IPC_CHANNEL);
                        LogBufferDescriptor.isConnected(metaDataBuffer, true);
                        untethered.state = UntetheredSubscription.ACTIVE;
                        untethered.timeOfLastUpdateNs = nowNs;
                    }
                    break;
            }
        }
    }

    public void onTimeEvent(final long timeNs, final long timeMs, final DriverConductor conductor)
    {
        switch (state)
        {
            case ACTIVE:
            {
                checkUntetheredSubscriptions(timeNs, conductor);
                updateConnectedStatus();
                final long producerPosition = producerPosition();
                publisherPos.setOrdered(producerPosition);
                if (!isExclusive)
                {
                    checkForBlockedPublisher(producerPosition, senderPosition.getVolatile(), timeNs);
                }
                break;
            }

            case DRAINING:
            {
                final long producerPosition = producerPosition();
                publisherPos.setOrdered(producerPosition);
                final long senderPosition = this.senderPosition.getVolatile();
                if (producerPosition > senderPosition)
                {
                    if (LogBufferUnblocker.unblock(termBuffers, metaDataBuffer, senderPosition, termBufferLength))
                    {
                        unblockedPublications.incrementOrdered();
                        break;
                    }

                    if (hasReceivers)
                    {
                        break;
                    }
                }
                else
                {
                    isEndOfStream = true;
                }

                if (spiesFinishedConsuming(conductor, producerPosition))
                {
                    timeOfLastActivityNs = timeNs;
                    state = State.LINGER;
                }
                break;
            }

            case LINGER:
                if ((timeOfLastActivityNs + lingerTimeoutNs) - timeNs < 0)
                {
                    conductor.cleanupPublication(this);
                    state = State.CLOSING;
                }
                break;
        }
    }

    public boolean hasReachedEndOfLife()
    {
        return hasSenderReleased;
    }

    public void decRef()
    {
        if (0 == --refCount)
        {
            state = State.DRAINING;
            channelEndpoint.decRef();
            timeOfLastActivityNs = nanoClock.nanoTime();

            final long producerPosition = producerPosition();
            if (publisherLimit.get() > producerPosition)
            {
                publisherLimit.setOrdered(producerPosition);
            }

            endOfStreamPosition(metaDataBuffer, producerPosition);

            if (senderPosition.getVolatile() >= producerPosition)
            {
                isEndOfStream = true;
            }
        }
    }

    public void incRef()
    {
        ++refCount;
    }

    final State state()
    {
        return state;
    }

    void senderRelease()
    {
        hasSenderReleased = true;
    }

    long producerPosition()
    {
        final long rawTail = rawTailVolatile(metaDataBuffer);
        final int termOffset = termOffset(rawTail, termBufferLength);

        return computePosition(termId(rawTail), termOffset, positionBitsToShift, initialTermId);
    }

    long consumerPosition()
    {
        return senderPosition.getVolatile();
    }
}


/*
 * Copyright 2014-2019 Real Logic Ltd.
 *
 * 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 io.aeron.driver.media;

import org.agrona.BufferUtil;

import java.net.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;

import static java.lang.Boolean.compare;
import static java.lang.Integer.compare;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.Collections.sort;

/**
 * Collection of network specific utility functions
 */
public class NetworkUtil
{
    /**
     * Search for a list of network interfaces that match the specified address and subnet prefix.
     * The results will be ordered by the length of the subnet prefix
     * ({@link InterfaceAddress#getNetworkPrefixLength()}).  If no results match, then the collection
     * will be empty.
     *
     * @param address      to search for on the {@link NetworkInterface}s.
     * @param subnetPrefix to limit the search.
     * @return {@link NetworkInterface}s that match the supplied criteria, ordered by the length
     * of the subnet prefix. Empty if none match.
     * @throws SocketException if an error occurs
     */
    public static NetworkInterface[] filterBySubnet(final InetAddress address, final int subnetPrefix)
        throws SocketException
    {
        return filterBySubnet(NetworkInterfaceShim.DEFAULT, address, subnetPrefix);
    }

    static NetworkInterface[] filterBySubnet(
        final NetworkInterfaceShim shim, final InetAddress address, final int subnetPrefix)
        throws SocketException
    {
        final ArrayList<FilterResult> filterResults = new ArrayList<>();
        final byte[] queryAddress = address.getAddress();

        final Enumeration<NetworkInterface> interfaces = shim.getNetworkInterfaces();
        while (interfaces.hasMoreElements())
        {
            final NetworkInterface networkInterface = interfaces.nextElement();
            final InterfaceAddress interfaceAddress = findAddressOnInterface(
                shim, networkInterface, queryAddress, subnetPrefix);

            if (null != interfaceAddress)
            {
                filterResults.add(new FilterResult(
                    interfaceAddress, networkInterface, shim.isLoopback(networkInterface)));
            }
        }

        sort(filterResults);

        final int size = filterResults.size();
        final NetworkInterface[] results = new NetworkInterface[size];
        for (int i = 0; i < size; i++)
        {
            results[i] = filterResults.get(i).networkInterface;
        }

        return results;
    }

    public static InetAddress findAddressOnInterface(
        final NetworkInterface networkInterface, final InetAddress address, final int subnetPrefix)
    {
        final InterfaceAddress interfaceAddress =
            findAddressOnInterface(NetworkInterfaceShim.DEFAULT, networkInterface, address.getAddress(), subnetPrefix);

        if (null == interfaceAddress)
        {
            return null;
        }

        return interfaceAddress.getAddress();
    }

    static InterfaceAddress findAddressOnInterface(
        final NetworkInterfaceShim shim,
        final NetworkInterface networkInterface,
        final byte[] queryAddress,
        final int prefixLength)
    {
        InterfaceAddress foundInterfaceAddress = null;

        for (final InterfaceAddress interfaceAddress : shim.getInterfaceAddresses(networkInterface))
        {
            final byte[] candidateAddress = interfaceAddress.getAddress().getAddress();
            if (isMatchWithPrefix(candidateAddress, queryAddress, prefixLength))
            {
                foundInterfaceAddress = interfaceAddress;
                break;
            }
        }

        return foundInterfaceAddress;
    }

    static boolean isMatchWithPrefix(final byte[] candidate, final byte[] expected, final int prefixLength)
    {
        if (candidate.length != expected.length)
        {
            return false;
        }

        if (candidate.length == 4)
        {
            final int mask = prefixLengthToIpV4Mask(prefixLength);

            return (toInt(candidate) & mask) == (toInt(expected) & mask);
        }
        else if (candidate.length == 16)
        {
            final long upperMask = prefixLengthToIpV6Mask(min(prefixLength, 64));
            final long lowerMask = prefixLengthToIpV6Mask(max(prefixLength - 64, 0));

            return
                (upperMask & toLong(candidate, 0)) == (upperMask & toLong(expected, 0)) &&
                    (lowerMask & toLong(candidate, 8)) == (lowerMask & toLong(expected, 8));
        }

        throw new IllegalArgumentException("How many bytes does an IP address have again?");
    }

    private static int prefixLengthToIpV4Mask(final int subnetPrefix)
    {
        return 0 == subnetPrefix ? 0 : -(1 << 32 - subnetPrefix);
    }

    private static long prefixLengthToIpV6Mask(final int subnetPrefix)
    {
        return 0 == subnetPrefix ? 0 : -(1L << 64 - subnetPrefix);
    }

    private static int toInt(final byte[] b)
    {
        return ((b[3] & 0xFF)) + ((b[2] & 0xFF) << 8) + ((b[1] & 0xFF) << 16) + ((b[0]) << 24);
    }

    static long toLong(final byte[] b, final int offset)
    {
        return ((b[offset + 7] & 0xFFL)) +
            ((b[offset + 6] & 0xFFL) << 8) +
            ((b[offset + 5] & 0xFFL) << 16) +
            ((b[offset + 4] & 0xFFL) << 24) +
            ((b[offset + 3] & 0xFFL) << 32) +
            ((b[offset + 2] & 0xFFL) << 40) +
            ((b[offset + 1] & 0xFFL) << 48) +
            (((long)b[offset]) << 56);
    }

    public static ProtocolFamily getProtocolFamily(final InetAddress address)
    {
        if (address instanceof Inet4Address)
        {
            return StandardProtocolFamily.INET;
        }
        else if (address instanceof Inet6Address)
        {
            return StandardProtocolFamily.INET6;
        }
        else
        {
            throw new IllegalStateException("Unknown ProtocolFamily");
        }
    }

    static class FilterResult implements Comparable<FilterResult>
    {
        private final InterfaceAddress interfaceAddress;
        private final NetworkInterface networkInterface;
        private final boolean isLoopback;

        FilterResult(
            final InterfaceAddress interfaceAddress,
            final NetworkInterface networkInterface,
            final boolean isLoopback)
        {
            this.interfaceAddress = interfaceAddress;
            this.networkInterface = networkInterface;
            this.isLoopback = isLoopback;
        }

        public int compareTo(final FilterResult other)
        {
            if (isLoopback == other.isLoopback)
            {
                return -compare(
                    interfaceAddress.getNetworkPrefixLength(),
                    other.interfaceAddress.getNetworkPrefixLength());
            }
            else
            {
                return compare(isLoopback, other.isLoopback);
            }
        }
    }

    /**
     * Allocate a direct {@link ByteBuffer} that is padded at the end with at least alignment bytes.
     *
     * @param capacity  for the buffer.
     * @param alignment for the buffer.
     * @return the direct {@link ByteBuffer}.
     */
    public static ByteBuffer allocateDirectAlignedAndPadded(final int capacity, final int alignment)
    {
        final ByteBuffer buffer = BufferUtil.allocateDirectAligned(capacity + alignment, alignment);

        buffer.limit(buffer.limit() - alignment);

        return buffer.slice();
    }
}


/*
 * Copyright 2014-2019 Real Logic Ltd.
 *
 * 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 io.aeron.driver.status;

import org.agrona.BitUtil;
import org.agrona.MutableDirectBuffer;
import org.agrona.concurrent.status.CountersManager;
import org.agrona.concurrent.status.CountersReader;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.status.UnsafeBufferPosition;

import static org.agrona.BitUtil.SIZE_OF_INT;
import static org.agrona.BitUtil.SIZE_OF_LONG;
import static org.agrona.concurrent.status.CountersReader.MAX_LABEL_LENGTH;

/**
 * Allocates counters on a stream of messages.
 *

 * Positions tracked in bytes include:
 *

 *
{@link PublisherPos}: Highest position on a {@link io.aeron.Publication} reached for offers and claims as an approximation sampled once per second.

 *
{@link PublisherLimit}: Limit for flow controlling a {@link io.aeron.Publication} offers and claims.

 *
{@link SenderPos}: Highest position on a {@link io.aeron.Publication} stream sent to the media.

 *
{@link SenderLimit}: Limit for flow controlling a {@link io.aeron.driver.Sender} of a stream.

 *
{@link ReceiverHwm}: Highest position observed by the Receiver when rebuilding an {@link io.aeron.Image} of a stream.

 *
{@link ReceiverPos}: Highest contiguous position rebuilt by the Receiver on an {@link io.aeron.Image} of a stream.

 *
{@link SubscriberPos}: Consumption position on an {@link io.aeron.Image} of a stream by individual Subscriber.

 *

 *

 * Note: All counters are real-time with the exception of {@link PublisherPos} which is sampled once per second
 * which means it can appear to be behind.
 */
public class StreamCounter
{
    /**
     * Offset in the key meta data for the registration id of the counter.
     */
    public static final int REGISTRATION_ID_OFFSET = 0;

    /**
     * Offset in the key meta data for the session id of the counter.
     */
    public static final int SESSION_ID_OFFSET = REGISTRATION_ID_OFFSET + SIZE_OF_LONG;

    /**
     * Offset in the key meta data for the stream id of the counter.
     */
    public static final int STREAM_ID_OFFSET = SESSION_ID_OFFSET + SIZE_OF_INT;

    /**
     * Offset in the key meta data for the channel of the counter.
     */
    public static final int CHANNEL_OFFSET = STREAM_ID_OFFSET + SIZE_OF_INT;

    /**
     * The maximum length in bytes of the encoded channel identity.
     */
    public static final int MAX_CHANNEL_LENGTH = CountersReader.MAX_KEY_LENGTH - (CHANNEL_OFFSET + SIZE_OF_INT);

    /**
     * Allocate a counter for tracking a position on a stream of messages.
     *
     * @param tempBuffer      to be used for labels and key.
     * @param name            of the counter for the label.
     * @param typeId          of the counter for classification.
     * @param countersManager from which to allocated the underlying storage.
     * @param registrationId  to be associated with the counter.
     * @param sessionId       for the stream of messages.
     * @param streamId        for the stream of messages.
     * @param channel         for the stream of messages.
     * @return a new {@link UnsafeBufferPosition} for tracking the stream.
     */
    public static UnsafeBufferPosition allocate(
        final MutableDirectBuffer tempBuffer,
        final String name,
        final int typeId,
        final CountersManager countersManager,
        final long registrationId,
        final int sessionId,
        final int streamId,
        final String channel)
    {
        return new UnsafeBufferPosition(
            (UnsafeBuffer)countersManager.valuesBuffer(),
            allocateCounterId(tempBuffer, name, typeId, countersManager, registrationId, sessionId, streamId, channel),
            countersManager);
    }

    public static int allocateCounterId(
        final MutableDirectBuffer tempBuffer,
        final String name,
        final int typeId,
        final CountersManager countersManager,
        final long registrationId,
        final int sessionId,
        final int streamId,
        final String channel)
    {
        tempBuffer.putLong(REGISTRATION_ID_OFFSET, registrationId);
        tempBuffer.putInt(SESSION_ID_OFFSET, sessionId);
        tempBuffer.putInt(STREAM_ID_OFFSET, streamId);

        final int channelLength = tempBuffer.putStringWithoutLengthAscii(
            CHANNEL_OFFSET + SIZE_OF_INT, channel, 0, MAX_CHANNEL_LENGTH);
        tempBuffer.putInt(CHANNEL_OFFSET, channelLength);
        final int keyLength = CHANNEL_OFFSET + SIZE_OF_INT + channelLength;

        final int labelOffset = BitUtil.align(keyLength, SIZE_OF_INT);
        int labelLength = 0;
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, name);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, ": ");
        labelLength += tempBuffer.putLongAscii(labelOffset + labelLength, registrationId);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " ");
        labelLength += tempBuffer.putIntAscii(labelOffset + labelLength, sessionId);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " ");
        labelLength += tempBuffer.putIntAscii(labelOffset + labelLength, streamId);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " ");
        labelLength += tempBuffer.putStringWithoutLengthAscii(
            labelOffset + labelLength, channel, 0, MAX_LABEL_LENGTH - labelLength);

        return countersManager.allocate(
            typeId,
            tempBuffer,
            0,
            keyLength,
            tempBuffer,
            labelOffset,
            labelLength);
    }

    /**
     * Allocate a counter for tracking a position on a stream of messages.
     *
     * @param tempBuffer      to be used for labels and key.
     * @param name            of the counter for the label.
     * @param typeId          of the counter for classification.
     * @param countersManager from which to allocated the underlying storage.
     * @param registrationId  to be associated with the counter.
     * @param sessionId       for the stream of messages.
     * @param streamId        for the stream of messages.
     * @param channel         for the stream of messages.
     * @param joinPosition    for the label.
     * @return a new {@link UnsafeBufferPosition} for tracking the stream.
     */
    public static UnsafeBufferPosition allocate(
        final MutableDirectBuffer tempBuffer,
        final String name,
        final int typeId,
        final CountersManager countersManager,
        final long registrationId,
        final int sessionId,
        final int streamId,
        final String channel,
        final long joinPosition)
    {
        tempBuffer.putLong(REGISTRATION_ID_OFFSET, registrationId);
        tempBuffer.putInt(SESSION_ID_OFFSET, sessionId);
        tempBuffer.putInt(STREAM_ID_OFFSET, streamId);

        final int channelLength = tempBuffer.putStringWithoutLengthAscii(
            CHANNEL_OFFSET + SIZE_OF_INT, channel, 0, MAX_CHANNEL_LENGTH);
        tempBuffer.putInt(CHANNEL_OFFSET, channelLength);
        final int keyLength = CHANNEL_OFFSET + SIZE_OF_INT + channelLength;

        final int labelOffset = BitUtil.align(keyLength, SIZE_OF_INT);
        int labelLength = 0;
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, name);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, ": ");
        labelLength += tempBuffer.putLongAscii(labelOffset + labelLength, registrationId);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " ");
        labelLength += tempBuffer.putIntAscii(labelOffset + labelLength, sessionId);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " ");
        labelLength += tempBuffer.putIntAscii(labelOffset + labelLength, streamId);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " ");
        labelLength += tempBuffer.putStringWithoutLengthAscii(
            labelOffset + labelLength, channel, 0, MAX_LABEL_LENGTH - labelLength);

        if (labelLength < (MAX_LABEL_LENGTH - 20))
        {
            labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " @");
            labelLength += tempBuffer.putLongAscii(labelOffset + labelLength, joinPosition);
        }

        final int counterId = countersManager.allocate(
            typeId,
            tempBuffer,
            0,
            keyLength,
            tempBuffer,
            labelOffset,
            labelLength);

        return new UnsafeBufferPosition((UnsafeBuffer)countersManager.valuesBuffer(), counterId, countersManager);
    }

    /**
     * Return the label name for a counter type identifier.
     *
     * @param typeId of the counter.
     * @return the label name as a String.
     */
    public static String labelName(final int typeId)
    {
        switch (typeId)
        {
            case PublisherLimit.PUBLISHER_LIMIT_TYPE_ID:
                return PublisherLimit.NAME;

            case SenderPos.SENDER_POSITION_TYPE_ID:
                return SenderPos.NAME;

            case ReceiverHwm.RECEIVER_HWM_TYPE_ID:
                return ReceiverHwm.NAME;

            case SubscriberPos.SUBSCRIBER_POSITION_TYPE_ID:
                return SubscriberPos.NAME;

            case ReceiverPos.RECEIVER_POS_TYPE_ID:
                return ReceiverPos.NAME;

            case SenderLimit.SENDER_LIMIT_TYPE_ID:
                return SenderLimit.NAME;

            case PublisherPos.PUBLISHER_POS_TYPE_ID:
                return PublisherPos.NAME;

            case SenderBpe.SENDER_BPE_TYPE_ID:
                return SenderBpe.NAME;

            default:
                return "<unknown>";
        }
    }
}


/*
 * Copyright 2014-2019 Real Logic Ltd.
 *
 * 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 io.aeron.logbuffer;

import org.agrona.concurrent.UnsafeBuffer;

import static io.aeron.logbuffer.FrameDescriptor.FRAME_ALIGNMENT;
import static io.aeron.logbuffer.FrameDescriptor.frameLengthVolatile;
import static io.aeron.logbuffer.FrameDescriptor.isPaddingFrame;
import static io.aeron.protocol.DataHeaderFlyweight.HEADER_LENGTH;
import static org.agrona.BitUtil.align;

/**
 * Scans a term buffer for an availability range of message fragments.
 * <p>
 * This can be used to concurrently read a term buffer which is being appended to.
 */
public final class TermScanner
{
    /**
     * Scan the term buffer for availability of new message fragments from a given offset up to a maxLength of bytes.
     *
     * @param termBuffer to be scanned for new message fragments.
     * @param offset     at which the scan should begin.
     * @param maxLength  in bytes of how much should be scanned.
     * @return resulting status of the scan which packs the available bytes and padding into a long.
     */
    public static long scanForAvailability(final UnsafeBuffer termBuffer, final int offset, final int maxLength)
    {
        final int limit = Math.min(maxLength, termBuffer.capacity() - offset);
        int available = 0;
        int padding = 0;

        do
        {
            final int termOffset = offset + available;
            final int frameLength = frameLengthVolatile(termBuffer, termOffset);
            if (frameLength <= 0)
            {
                break;
            }

            int alignedFrameLength = align(frameLength, FRAME_ALIGNMENT);
            if (isPaddingFrame(termBuffer, termOffset))
            {
                padding = alignedFrameLength - HEADER_LENGTH;
                alignedFrameLength = HEADER_LENGTH;
            }

            available += alignedFrameLength;

            if (available > limit)
            {
                available -= alignedFrameLength;
                padding = 0;
                break;
            }
        }
        while (0 == padding && available < limit);

        return pack(padding, available);
    }

    /**
     * Pack the values for available and padding into a long for returning on the stack.
     *
     * @param padding   value to be packed.
     * @param available value to be packed.
     * @return a long with both ints packed into it.
     */
    public static long pack(final int padding, final int available)
    {
        return ((long)padding << 32) | available;
    }

    /**
     * The number of bytes that are available to be read after a scan.
     *
     * @param result into which the padding value has been packed.
     * @return the count of bytes that are available to be read.
     */
    public static int available(final long result)
    {
        return (int)result;
    }

    /**
     * The count of bytes that should be added for padding to the position on top of what is available.
     *
     * @param result into which the padding value has been packed.
     * @return the count of bytes that should be added for padding to the position on top of what is available.
     */
    public static int padding(final long result)
    {
        return (int)(result >>> 32);
    }
}