/*
 * 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 io.aeron.ChannelUri;
import io.aeron.CommonContext;
import io.aeron.ErrorCode;
import io.aeron.driver.exceptions.InvalidChannelException;
import org.agrona.BitUtil;

import java.net.*;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

import static io.aeron.driver.media.NetworkUtil.*;
import static java.lang.System.lineSeparator;
import static java.net.InetAddress.getByAddress;

/**
 * The media configuration for Aeron UDP channels as an instantiation of the socket addresses for a {@link ChannelUri}.
 *
 * @see ChannelUri
 * @see io.aeron.ChannelUriStringBuilder
 */
public final class UdpChannel
{
    private static final byte[] HEX_TABLE =
    {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };

    private static final AtomicInteger UNIQUE_CANONICAL_FORM_VALUE = new AtomicInteger();

    private final boolean hasExplicitControl;
    private final boolean isMulticast;
    private final boolean hasMulticastTtl;
    private final boolean hasTag;
    private final int multicastTtl;
    private final long tag;
    private final InetSocketAddress remoteData;
    private final InetSocketAddress localData;
    private final InetSocketAddress remoteControl;
    private final InetSocketAddress localControl;
    private final String uriStr;
    private final String canonicalForm;
    private final NetworkInterface localInterface;
    private final ProtocolFamily protocolFamily;
    private final ChannelUri channelUri;

    private UdpChannel(final Context context)
    {
        hasExplicitControl = context.hasExplicitControl;
        isMulticast = context.isMulticast;
        hasTag = context.hasTagId;
        tag = context.tagId;
        hasMulticastTtl = context.hasMulticastTtl;
        multicastTtl = context.multicastTtl;
        remoteData = context.remoteData;
        localData = context.localData;
        remoteControl = context.remoteControl;
        localControl = context.localControl;
        uriStr = context.uriStr;
        canonicalForm = context.canonicalForm;
        localInterface = context.localInterface;
        protocolFamily = context.protocolFamily;
        channelUri = context.channelUri;
    }

    /**
     * Parse channel URI and create a {@link UdpChannel}.
     *
     * @param channelUriString to parse
     * @return a new {@link UdpChannel}
     * @throws InvalidChannelException if an error occurs.
     */
    @SuppressWarnings("MethodLength")
    public static UdpChannel parse(final String channelUriString)
    {
        try
        {
            final ChannelUri channelUri = ChannelUri.parse(channelUriString);
            validateConfiguration(channelUri);

            InetSocketAddress endpointAddress = getEndpointAddress(channelUri);
            final InetSocketAddress explicitControlAddress = getExplicitControlAddress(channelUri);

            final String tagIdStr = channelUri.channelTag();
            final String controlMode = channelUri.get(CommonContext.MDC_CONTROL_MODE_PARAM_NAME);
            final boolean hasNoDistinguishingCharacteristic =
                null == endpointAddress && null == explicitControlAddress && null == tagIdStr;

            if (hasNoDistinguishingCharacteristic && null == controlMode)
            {
                throw new IllegalArgumentException(
                    "Aeron URIs for UDP must specify an endpoint address, control address, tag-id, or control-mode");
            }

            if (null != endpointAddress && endpointAddress.isUnresolved())
            {
                throw new UnknownHostException("could not resolve endpoint address: " + endpointAddress);
            }

            if (null != explicitControlAddress && explicitControlAddress.isUnresolved())
            {
                throw new UnknownHostException("could not resolve control address: " + explicitControlAddress);
            }

            final Context context = new Context()
                .uriStr(channelUriString)
                .channelUri(channelUri)
                .hasNoDistinguishingCharacteristic(hasNoDistinguishingCharacteristic);

            if (null != tagIdStr)
            {
                context.hasTagId(true).tagId(Long.parseLong(tagIdStr));
            }

            if (null == endpointAddress)
            {
                endpointAddress = new InetSocketAddress("0.0.0.0", 0);
            }

            if (endpointAddress.getAddress().isMulticastAddress())
            {
                final InetSocketAddress controlAddress = getMulticastControlAddress(endpointAddress);
                final InterfaceSearchAddress searchAddress = getInterfaceSearchAddress(channelUri);
                final NetworkInterface localInterface = findInterface(searchAddress);
                final InetSocketAddress resolvedAddress = resolveToAddressOfInterface(localInterface, searchAddress);

                context
                    .isMulticast(true)
                    .localControlAddress(resolvedAddress)
                    .remoteControlAddress(controlAddress)
                    .localDataAddress(resolvedAddress)
                    .remoteDataAddress(endpointAddress)
                    .localInterface(localInterface)
                    .protocolFamily(getProtocolFamily(endpointAddress.getAddress()))
                    .canonicalForm(canonicalise(resolvedAddress, endpointAddress));

                final String ttlValue = channelUri.get(CommonContext.TTL_PARAM_NAME);
                if (null != ttlValue)
                {
                    context.hasMulticastTtl(true).multicastTtl(Integer.parseInt(ttlValue));
                }
            }
            else if (null != explicitControlAddress)
            {
                context
                    .hasExplicitControl(true)
                    .remoteControlAddress(endpointAddress)
                    .remoteDataAddress(endpointAddress)
                    .localControlAddress(explicitControlAddress)
                    .localDataAddress(explicitControlAddress)
                    .protocolFamily(getProtocolFamily(endpointAddress.getAddress()))
                    .canonicalForm(canonicalise(explicitControlAddress, endpointAddress));
            }
            else
            {
                final InterfaceSearchAddress searchAddress = getInterfaceSearchAddress(channelUri);

                final InetSocketAddress localAddress = searchAddress.getInetAddress().isAnyLocalAddress() ?
                    searchAddress.getAddress() :
                    resolveToAddressOfInterface(findInterface(searchAddress), searchAddress);

                final String uniqueCanonicalFormSuffix = hasNoDistinguishingCharacteristic ?
                    ("-" + UNIQUE_CANONICAL_FORM_VALUE.getAndAdd(1)) : "";

                context
                    .remoteControlAddress(endpointAddress)
                    .remoteDataAddress(endpointAddress)
                    .localControlAddress(localAddress)
                    .localDataAddress(localAddress)
                    .protocolFamily(getProtocolFamily(endpointAddress.getAddress()))
                    .canonicalForm(canonicalise(localAddress, endpointAddress) + uniqueCanonicalFormSuffix);
            }

            return new UdpChannel(context);
        }
        catch (final Exception ex)
        {
            throw new InvalidChannelException(ErrorCode.INVALID_CHANNEL, ex);
        }
    }

    private static InetSocketAddress getMulticastControlAddress(final InetSocketAddress endpointAddress)
        throws UnknownHostException
    {
        final byte[] addressAsBytes = endpointAddress.getAddress().getAddress();
        validateDataAddress(addressAsBytes);

        addressAsBytes[addressAsBytes.length - 1]++;
        return new InetSocketAddress(getByAddress(addressAsBytes), endpointAddress.getPort());
    }

    /**
     * Return a string which is a canonical form of the channel suitable for use as a file or directory
     * name and also as a method of hashing, etc.
     *

     * A canonical form:
     *

     *
begins with the string "UDP-"

     *
has all addresses converted to hexadecimal

     *
uses "-" as all field separators

     *

     *

     * The general format is:
     * UDP-interface-localPort-remoteAddress-remotePort
     *
     * @param localData  address/interface for the channel
     * @param remoteData address for the channel
     * @return canonical representation as a string
     */
    public static String canonicalise(final InetSocketAddress localData, final InetSocketAddress remoteData)
    {
        final StringBuilder builder = new StringBuilder(48);

        builder.append("UDP-");

        toHex(builder, localData.getAddress().getAddress())
            .append('-')
            .append(localData.getPort());

        builder.append('-');

        toHex(builder, remoteData.getAddress().getAddress())
            .append('-')
            .append(remoteData.getPort());

        return builder.toString();
    }

    /**
     * Remote data address information
     *
     * @return remote data address information
     */
    public InetSocketAddress remoteData()
    {
        return remoteData;
    }

    /**
     * Local data address information
     *
     * @return local data address information
     */
    public InetSocketAddress localData()
    {
        return localData;
    }

    /**
     * Remote control address information
     *
     * @return remote control address information
     */
    public InetSocketAddress remoteControl()
    {
        return remoteControl;
    }

    /**
     * Local control address information
     *
     * @return local control address information
     */
    public InetSocketAddress localControl()
    {
        return localControl;
    }

    /**
     * Get the {@link ChannelUri} for this channel.
     *
     * @return the {@link ChannelUri} for this channel.
     */
    public ChannelUri channelUri()
    {
        return channelUri;
    }

    /**
     * Has this channel got a multicast TTL value set so that {@link #multicastTtl()} is valid.
     *
     * @return true if this channel is a multicast TTL set otherwise false.
     */
    public boolean isHasMulticastTtl()
    {
        return hasMulticastTtl;
    }

    /**
     * Multicast TTL information
     *
     * @return multicast TTL value
     */
    public int multicastTtl()
    {
        return multicastTtl;
    }

    /**
     * The canonical form for the channel
     *

     * {@link UdpChannel#canonicalise(java.net.InetSocketAddress, java.net.InetSocketAddress)}
     *
     * @return canonical form for channel
     */
    public String canonicalForm()
    {
        return canonicalForm;
    }

    public boolean equals(final Object o)
    {
        if (this == o)
        {
            return true;
        }

        if (o == null || getClass() != o.getClass())
        {
            return false;
        }

        final UdpChannel that = (UdpChannel)o;

        return Objects.equals(canonicalForm, that.canonicalForm);
    }

    public int hashCode()
    {
        return canonicalForm != null ? canonicalForm.hashCode() : 0;
    }

    public String toString()
    {
        return canonicalForm;
    }

    /**
     * Does channel represent a multicast or not
     *
     * @return does channel represent a multicast or not
     */
    public boolean isMulticast()
    {
        return isMulticast;
    }

    /**
     * Local interface to be used by the channel.
     *
     * @return {@link NetworkInterface} for the local interface used by the channel
     */
    public NetworkInterface localInterface()
    {
        return localInterface;
    }

    /**
     * Original URI of the channel URI.
     *
     * @return the original uri
     */
    public String originalUriString()
    {
        return uriStr;
    }

    /**
     * Get the {@link ProtocolFamily} for this channel.
     *
     * @return the {@link ProtocolFamily} for this channel.
     */
    public ProtocolFamily protocolFamily()
    {
        return protocolFamily;
    }

    /**
     * Get the tag value on the channel which is only valid if {@link #hasTag()} is true.
     *
     * @return the tag value on the channel.
     */
    public long tag()
    {
        return tag;
    }

    /**
     * Does the channel have an explicit control address as used with multi-destination-cast or not?
     *
     * @return does channel have an explicit control address or not?
     */
    public boolean hasExplicitControl()
    {
        return hasExplicitControl;
    }

    /**
     * Has the URI a tag to indicate entity relationships and if {@link #tag()} is valid.
     *
     * @return true if the channel has a tag.
     */
    public boolean hasTag()
    {
        return hasTag;
    }

    /**
     * Does this channel have a tag match to another channel including endpoints.
     *
     * @param udpChannel to match against.
     * @return true if there is a match otherwise false.
     */
    public boolean matchesTag(final UdpChannel udpChannel)
    {
        if (!hasTag || !udpChannel.hasTag() || tag != udpChannel.tag())
        {
            return false;
        }

        if (udpChannel.remoteData().getAddress().isAnyLocalAddress() &&
            udpChannel.remoteData().getPort() == 0 &&
            udpChannel.localData().getAddress().isAnyLocalAddress() &&
            udpChannel.localData().getPort() == 0)
        {
            return true;
        }

        throw new IllegalArgumentException("matching tag has set endpoint or control address");
    }

    /**
     * Get the endpoint address from the URI.
     *
     * @param uri to check
     * @return endpoint address for URI
     */
    public static InetSocketAddress destinationAddress(final ChannelUri uri)
    {
        try
        {
            validateConfiguration(uri);
            return getEndpointAddress(uri);
        }
        catch (final Exception ex)
        {
            throw new InvalidChannelException(ErrorCode.INVALID_CHANNEL, ex);
        }
    }

    /**
     * Used for debugging to get a human readable description of the channel.
     *
     * @return a human readable description of the channel.
     */
    public String description()
    {
        final StringBuilder builder = new StringBuilder("UdpChannel - ");
        if (null != localInterface)
        {
            builder
                .append("interface: ")
                .append(localInterface.getDisplayName())
                .append(", ");
        }

        builder
            .append("localData: ").append(localData)
            .append(", remoteData: ").append(remoteData)
            .append(", ttl: ").append(multicastTtl);

        return builder.toString();
    }

    private static InterfaceSearchAddress getInterfaceSearchAddress(final ChannelUri uri) throws UnknownHostException
    {
        final String interfaceValue = uri.get(CommonContext.INTERFACE_PARAM_NAME);
        if (null != interfaceValue)
        {
            return InterfaceSearchAddress.parse(interfaceValue);
        }

        return InterfaceSearchAddress.wildcard();
    }

    private static InetSocketAddress getEndpointAddress(final ChannelUri uri)
    {
        final String endpointValue = uri.get(CommonContext.ENDPOINT_PARAM_NAME);
        if (null != endpointValue)
        {
            return SocketAddressUtil.parse(endpointValue);
        }

        return null;
    }

    private static InetSocketAddress getExplicitControlAddress(final ChannelUri uri)
    {
        final String controlValue = uri.get(CommonContext.MDC_CONTROL_PARAM_NAME);
        if (null != controlValue)
        {
            return SocketAddressUtil.parse(controlValue);
        }

        return null;
    }

    private static void validateDataAddress(final byte[] addressAsBytes)
    {
        if (BitUtil.isEven(addressAsBytes[addressAsBytes.length - 1]))
        {
            throw new IllegalArgumentException("Multicast data address must be odd");
        }
    }

    private static void validateConfiguration(final ChannelUri uri)
    {
        validateMedia(uri);
    }

    private static void validateMedia(final ChannelUri uri)
    {
        if (!CommonContext.UDP_MEDIA.equals(uri.media()))
        {
            throw new IllegalArgumentException("UdpChannel only supports UDP media: " + uri);
        }
    }

    private static InetSocketAddress resolveToAddressOfInterface(
        final NetworkInterface localInterface, final InterfaceSearchAddress searchAddress)
    {
        final InetAddress interfaceAddress = findAddressOnInterface(
            localInterface, searchAddress.getInetAddress(), searchAddress.getSubnetPrefix());

        if (null == interfaceAddress)
        {
            throw new IllegalStateException();
        }

        return new InetSocketAddress(interfaceAddress, searchAddress.getPort());
    }

    private static NetworkInterface findInterface(final InterfaceSearchAddress searchAddress)
        throws SocketException
    {
        final NetworkInterface[] filteredInterfaces = filterBySubnet(
            searchAddress.getInetAddress(), searchAddress.getSubnetPrefix());

        for (final NetworkInterface networkInterface : filteredInterfaces)
        {
            if (networkInterface.supportsMulticast() || networkInterface.isLoopback())
            {
                return networkInterface;
            }
        }

        throw new IllegalArgumentException(errorNoMatchingInterfaces(filteredInterfaces, searchAddress));
    }

    private static String errorNoMatchingInterfaces(
        final NetworkInterface[] filteredInterfaces, final InterfaceSearchAddress address)
        throws SocketException
    {
        final StringBuilder builder = new StringBuilder()
            .append("Unable to find multicast interface matching criteria: ")
            .append(address.getAddress())
            .append('/')
            .append(address.getSubnetPrefix());

        if (filteredInterfaces.length > 0)
        {
            builder.append(lineSeparator()).append("  Candidates:");

            for (final NetworkInterface ifc : filteredInterfaces)
            {
                builder
                    .append(lineSeparator())
                    .append("  - Name: ")
                    .append(ifc.getDisplayName())
                    .append(", addresses: ")
                    .append(ifc.getInterfaceAddresses())
                    .append(", multicast: ")
                    .append(ifc.supportsMulticast());
            }
        }

        return builder.toString();
    }

    private static StringBuilder toHex(final StringBuilder builder, final byte[] bytes)
    {
        for (final byte b : bytes)
        {
            builder.append((char)(HEX_TABLE[(b >> 4) & 0x0F]));
            builder.append((char)(HEX_TABLE[b & 0x0F]));
        }

        return builder;
    }

    static class Context
    {
        long tagId;
        int multicastTtl;
        InetSocketAddress remoteData;
        InetSocketAddress localData;
        InetSocketAddress remoteControl;
        InetSocketAddress localControl;
        String uriStr;
        String canonicalForm;
        NetworkInterface localInterface;
        ProtocolFamily protocolFamily;
        ChannelUri channelUri;
        boolean hasExplicitControl = false;
        boolean isMulticast = false;
        boolean hasMulticastTtl = false;
        boolean hasTagId = false;
        boolean hasNoDistinguishingCharacteristic = false;

        Context uriStr(final String uri)
        {
            uriStr = uri;
            return this;
        }

        Context remoteDataAddress(final InetSocketAddress remoteData)
        {
            this.remoteData = remoteData;
            return this;
        }

        Context localDataAddress(final InetSocketAddress localData)
        {
            this.localData = localData;
            return this;
        }

        Context remoteControlAddress(final InetSocketAddress remoteControl)
        {
            this.remoteControl = remoteControl;
            return this;
        }

        Context localControlAddress(final InetSocketAddress localControl)
        {
            this.localControl = localControl;
            return this;
        }

        Context canonicalForm(final String canonicalForm)
        {
            this.canonicalForm = canonicalForm;
            return this;
        }

        Context localInterface(final NetworkInterface networkInterface)
        {
            this.localInterface = networkInterface;
            return this;
        }

        Context protocolFamily(final ProtocolFamily protocolFamily)
        {
            this.protocolFamily = protocolFamily;
            return this;
        }

        Context hasMulticastTtl(final boolean hasMulticastTtl)
        {
            this.hasMulticastTtl = hasMulticastTtl;
            return this;
        }

        Context multicastTtl(final int multicastTtl)
        {
            this.multicastTtl = multicastTtl;
            return this;
        }

        Context tagId(final long tagId)
        {
            this.tagId = tagId;
            return this;
        }

        Context channelUri(final ChannelUri channelUri)
        {
            this.channelUri = channelUri;
            return this;
        }

        Context hasExplicitControl(final boolean hasExplicitControl)
        {
            this.hasExplicitControl = hasExplicitControl;
            return this;
        }

        Context isMulticast(final boolean isMulticast)
        {
            this.isMulticast = isMulticast;
            return this;
        }

        Context hasTagId(final boolean hasTagId)
        {
            this.hasTagId = hasTagId;
            return this;
        }

        Context hasNoDistinguishingCharacteristic(final boolean hasNoDistinguishingCharacteristic)
        {
            this.hasNoDistinguishingCharacteristic = hasNoDistinguishingCharacteristic;
            return this;
        }
    }
}


/*
 * 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.Aeron;
import io.aeron.driver.buffer.RawLog;
import io.aeron.driver.media.ImageConnection;
import io.aeron.driver.media.ReceiveChannelEndpoint;
import io.aeron.driver.media.ReceiveDestinationUdpTransport;
import io.aeron.driver.reports.LossReport;
import io.aeron.driver.status.SystemCounters;
import io.aeron.logbuffer.LogBufferDescriptor;
import io.aeron.logbuffer.TermRebuilder;
import io.aeron.protocol.DataHeaderFlyweight;
import io.aeron.protocol.RttMeasurementFlyweight;
import org.agrona.collections.ArrayListUtil;
import org.agrona.collections.ArrayUtil;
import org.agrona.concurrent.EpochClock;
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.util.ArrayList;

import static io.aeron.driver.LossDetector.lossFound;
import static io.aeron.driver.LossDetector.rebuildOffset;
import static io.aeron.driver.PublicationImage.State.ACTIVE;
import static io.aeron.driver.PublicationImage.State.INIT;
import static io.aeron.driver.status.SystemCounterDescriptor.*;
import static io.aeron.logbuffer.LogBufferDescriptor.*;
import static io.aeron.logbuffer.TermGapFiller.tryFillGap;
import static org.agrona.UnsafeAccess.UNSAFE;

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

class PublicationImageConductorFields extends PublicationImagePadding1
{
    protected long cleanPosition;
    protected final ArrayList<UntetheredSubscription> untetheredSubscriptions = new ArrayList<>();
    protected ReadablePosition[] subscriberPositions;
    protected LossReport lossReport;
    protected LossReport.ReportEntry reportEntry;
}

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

class PublicationImageReceiverFields extends PublicationImagePadding2
{
    protected boolean isEndOfStream = false;
    protected long lastPacketTimestampNs;
    protected ImageConnection[] imageConnections = new ImageConnection[1];
}

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

/**
 * State maintained for active sessionIds within a channel for receiver processing
 */
public class PublicationImage
    extends PublicationImagePadding3
    implements LossHandler, DriverManagedResource, Subscribable
{
    enum State
    {
        INIT, ACTIVE, INACTIVE, LINGER, DONE
    }

    private volatile long beginLossChange = Aeron.NULL_VALUE;
    private volatile long endLossChange = Aeron.NULL_VALUE;
    private int lossTermId;
    private int lossTermOffset;
    private int lossLength;

    private volatile long beginSmChange = Aeron.NULL_VALUE;
    private volatile long endSmChange = Aeron.NULL_VALUE;
    private long nextSmPosition;
    private int nextSmReceiverWindowLength;
    private long timeOfLastStatusMessageScheduleNs;

    private long lastLossChangeNumber = Aeron.NULL_VALUE;
    private long lastSmChangeNumber = Aeron.NULL_VALUE;
    private long lastSmPosition;
    private long lastSmWindowLimit;

    private long timeOfLastStateChangeNs;

    private final long correlationId;
    private final long imageLivenessTimeoutNs;
    private final long untetheredWindowLimitTimeoutNs;
    private final long untetheredRestingTimeoutNs;
    private final int sessionId;
    private final int streamId;
    private final int positionBitsToShift;
    private final int termLengthMask;
    private final int initialTermId;
    private final boolean isReliable;

    private boolean isTrackingRebuild = true;
    private volatile State state = INIT;

    private final NanoClock nanoClock;
    private final NanoClock cachedNanoClock;
    private final ReceiveChannelEndpoint channelEndpoint;
    private final UnsafeBuffer[] termBuffers;
    private final Position hwmPosition;
    private final LossDetector lossDetector;
    private final CongestionControl congestionControl;
    private final Position rebuildPosition;
    private final InetSocketAddress sourceAddress;
    private final AtomicCounter heartbeatsReceived;
    private final AtomicCounter statusMessagesSent;
    private final AtomicCounter nakMessagesSent;
    private final AtomicCounter flowControlUnderRuns;
    private final AtomicCounter flowControlOverRuns;
    private final AtomicCounter lossGapFills;
    private final EpochClock cachedEpochClock;
    private final RawLog rawLog;

    public PublicationImage(
        final long correlationId,
        final long imageLivenessTimeoutNs,
        final long untetheredWindowLimitTimeoutNs,
        final long untetheredRestingTimeoutNs,
        final ReceiveChannelEndpoint channelEndpoint,
        final int transportIndex,
        final InetSocketAddress controlAddress,
        final int sessionId,
        final int streamId,
        final int initialTermId,
        final int activeTermId,
        final int initialTermOffset,
        final RawLog rawLog,
        final FeedbackDelayGenerator lossFeedbackDelayGenerator,
        final ArrayList<SubscriberPosition> subscriberPositions,
        final Position hwmPosition,
        final Position rebuildPosition,
        final NanoClock nanoClock,
        final NanoClock cachedNanoClock,
        final EpochClock cachedEpochClock,
        final SystemCounters systemCounters,
        final InetSocketAddress sourceAddress,
        final CongestionControl congestionControl,
        final LossReport lossReport)
    {
        this.correlationId = correlationId;
        this.imageLivenessTimeoutNs = imageLivenessTimeoutNs;
        this.untetheredWindowLimitTimeoutNs = untetheredWindowLimitTimeoutNs;
        this.untetheredRestingTimeoutNs = untetheredRestingTimeoutNs;
        this.channelEndpoint = channelEndpoint;
        this.sessionId = sessionId;
        this.streamId = streamId;
        this.rawLog = rawLog;
        this.hwmPosition = hwmPosition;
        this.rebuildPosition = rebuildPosition;
        this.sourceAddress = sourceAddress;
        this.initialTermId = initialTermId;
        this.congestionControl = congestionControl;
        this.lossReport = lossReport;

        this.nanoClock = nanoClock;
        this.cachedNanoClock = cachedNanoClock;
        this.cachedEpochClock = cachedEpochClock;

        final long nowNs = cachedNanoClock.nanoTime();
        this.timeOfLastStateChangeNs = nowNs;
        this.lastPacketTimestampNs = nowNs;

        this.subscriberPositions = positionArray(subscriberPositions, nowNs);
        this.isReliable = subscriberPositions.get(0).subscription().isReliable();

        heartbeatsReceived = systemCounters.get(HEARTBEATS_RECEIVED);
        statusMessagesSent = systemCounters.get(STATUS_MESSAGES_SENT);
        nakMessagesSent = systemCounters.get(NAK_MESSAGES_SENT);
        flowControlUnderRuns = systemCounters.get(FLOW_CONTROL_UNDER_RUNS);
        flowControlOverRuns = systemCounters.get(FLOW_CONTROL_OVER_RUNS);
        lossGapFills = systemCounters.get(LOSS_GAP_FILLS);

        imageConnections = ArrayUtil.ensureCapacity(imageConnections, transportIndex + 1);
        imageConnections[transportIndex] = new ImageConnection(nowNs, controlAddress);

        termBuffers = rawLog.termBuffers();
        lossDetector = new LossDetector(lossFeedbackDelayGenerator, this);

        final int termLength = rawLog.termLength();
        termLengthMask = termLength - 1;
        positionBitsToShift = LogBufferDescriptor.positionBitsToShift(termLength);

        final long position = computePosition(activeTermId, initialTermOffset, positionBitsToShift, initialTermId);
        nextSmPosition = position;
        nextSmReceiverWindowLength = congestionControl.initialWindowLength();
        lastSmPosition = position;
        lastSmWindowLimit = position + nextSmReceiverWindowLength;
        cleanPosition = position;

        hwmPosition.setOrdered(position);
        rebuildPosition.setOrdered(position);
    }

    /**
     * {@inheritDoc}
     */
    public boolean free()
    {
        return rawLog.free();
    }

    /**
     * {@inheritDoc}
     */
    public void close()
    {
        hwmPosition.close();
        rebuildPosition.close();
        for (final ReadablePosition position : subscriberPositions)
        {
            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();
            }
        }

        congestionControl.close();
        rawLog.close();
    }

    /**
     * The correlation id assigned by the driver when created.
     *
     * @return the correlation id assigned by the driver when created.
     */
    public long correlationId()
    {
        return correlationId;
    }

    /**
     * The session id of the channel from a publisher.
     *
     * @return session id of the channel from a publisher.
     */
    public int sessionId()
    {
        return sessionId;
    }

    /**
     * The stream id of this image within a channel.
     *
     * @return stream id of this image within a channel.
     */
    public int streamId()
    {
        return streamId;
    }

    /**
     * Get the string representation of the channel URI.
     *
     * @return the string representation of the channel URI.
     */
    public String channel()
    {
        return channelEndpoint.originalUriString();
    }

    /**
     * {@inheritDoc}
     */
    public void addSubscriber(final SubscriptionLink subscriptionLink, final ReadablePosition subscriberPosition)
    {
        subscriberPositions = ArrayUtil.add(subscriberPositions, subscriberPosition);
        if (!subscriptionLink.isTether())
        {
            untetheredSubscriptions.add(new UntetheredSubscription(
                subscriptionLink, subscriberPosition, timeOfLastStatusMessageScheduleNs));
        }
    }

    /**
     * {@inheritDoc}
     */
    public void removeSubscriber(final SubscriptionLink subscriptionLink, final ReadablePosition subscriberPosition)
    {
        subscriberPositions = ArrayUtil.remove(subscriberPositions, subscriberPosition);
        subscriberPosition.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;
                }
            }
        }
    }

    /**
     * Called from the {@link LossDetector} when gap is detected by the {@link DriverConductor} thread.
     *
     * {@inheritDoc}
     */
    public void onGapDetected(final int termId, final int termOffset, final int length)
    {
        final long changeNumber = beginLossChange + 1;

        beginLossChange = changeNumber;

        lossTermId = termId;
        lossTermOffset = termOffset;
        lossLength = length;

        endLossChange = changeNumber;

        if (null != reportEntry)
        {
            reportEntry.recordObservation(length, cachedEpochClock.time());
        }
        else if (null != lossReport)
        {
            reportEntry = lossReport.createEntry(
                length, cachedEpochClock.time(), sessionId, streamId, channel(), sourceAddress.toString());

            if (null == reportEntry)
            {
                lossReport = null;
            }
        }
    }

    /**
     * The address of the source associated with the image.
     *
     * @return source address
     */
    InetSocketAddress sourceAddress()
    {
        return sourceAddress;
    }

    /**
     * Return the {@link ReceiveChannelEndpoint} that the image is attached to.
     *
     * @return {@link ReceiveChannelEndpoint} that the image is attached to.
     */
    ReceiveChannelEndpoint channelEndpoint()
    {
        return channelEndpoint;
    }

    /**
     * Remove this image from the {@link DataPacketDispatcher} so it will process no further packets from the network.
     * Called from the {@link Receiver} thread.
     */
    void removeFromDispatcher()
    {
        channelEndpoint.removePublicationImage(this);
    }

    /**
     * Get the {@link RawLog} the back this image.
     *
     * @return the {@link RawLog} the back this image.
     */
    RawLog rawLog()
    {
        return rawLog;
    }

    /**
     * Activate this image from the {@link Receiver}
     */
    void activate()
    {
        state(ACTIVE);
    }

    /**
     * Add a destination to this image so it can merge streams.
     *
     * @param transportIndex from which packets will arrive.
     * @param transport      from which packets will arrive.
     */
    void addDestination(final int transportIndex, final ReceiveDestinationUdpTransport transport)
    {
        imageConnections = ArrayUtil.ensureCapacity(imageConnections, transportIndex + 1);

        if (transport.isMulticast())
        {
            imageConnections[transportIndex] = new ImageConnection(
                cachedNanoClock.nanoTime(), transport.udpChannel().remoteControl());
        }
        else if (transport.hasExplicitControl())
        {
            imageConnections[transportIndex] = new ImageConnection(
                cachedNanoClock.nanoTime(), transport.explicitControlAddress());
        }
    }

    /**
     * Remove a destination to this image so it can merge streams.
     *
     * @param transportIndex from which packets arrive.
     */
    void removeDestination(final int transportIndex)
    {
        imageConnections[transportIndex] = null;
    }

    void addDestinationConnectionIfUnknown(final int transportIndex, final InetSocketAddress remoteAddress)
    {
        trackConnection(transportIndex, remoteAddress, cachedNanoClock.nanoTime());
    }

    /**
     * Called from the {@link DriverConductor}.
     *
     * @param nowNs                  in nanoseconds
     * @param statusMessageTimeoutNs for sending of Status Messages.
     */
    final void trackRebuild(final long nowNs, final long statusMessageTimeoutNs)
    {
        long minSubscriberPosition = Long.MAX_VALUE;
        long maxSubscriberPosition = Long.MIN_VALUE;

        for (final ReadablePosition subscriberPosition : subscriberPositions)
        {
            final long position = subscriberPosition.getVolatile();
            minSubscriberPosition = Math.min(minSubscriberPosition, position);
            maxSubscriberPosition = Math.max(maxSubscriberPosition, position);
        }

        final long rebuildPosition = Math.max(this.rebuildPosition.get(), maxSubscriberPosition);
        final long hwmPosition = this.hwmPosition.getVolatile();

        final long scanOutcome = lossDetector.scan(
            termBuffers[indexByPosition(rebuildPosition, positionBitsToShift)],
            rebuildPosition,
            hwmPosition,
            nowNs,
            termLengthMask,
            positionBitsToShift,
            initialTermId);

        final int rebuildTermOffset = (int)rebuildPosition & termLengthMask;
        final long newRebuildPosition = (rebuildPosition - rebuildTermOffset) + rebuildOffset(scanOutcome);
        this.rebuildPosition.proposeMaxOrdered(newRebuildPosition);

        final long ccOutcome = congestionControl.onTrackRebuild(
            nowNs,
            minSubscriberPosition,
            nextSmPosition,
            hwmPosition,
            rebuildPosition,
            newRebuildPosition,
            lossFound(scanOutcome));

        final int windowLength = CongestionControl.receiverWindowLength(ccOutcome);
        final int threshold = CongestionControl.threshold(windowLength);

        if (CongestionControl.shouldForceStatusMessage(ccOutcome) ||
            ((timeOfLastStatusMessageScheduleNs + statusMessageTimeoutNs) - nowNs < 0) ||
            (minSubscriberPosition > (nextSmPosition + threshold)))
        {
            scheduleStatusMessage(nowNs, minSubscriberPosition, windowLength);
            cleanBufferTo(minSubscriberPosition - (termLengthMask + 1));
        }
    }

    /**
     * Set state to {@link State#INACTIVE} if currently {@link State#ACTIVE}. Set by {@link Receiver}.
     */
    void ifActiveGoInactive()
    {
        if (State.ACTIVE == state)
        {
            state(State.INACTIVE);
        }
    }

    /**
     * Is this image actively rebuilding and thus should be checked for loss.
     *
     * @return true if this image actively rebuilding and thus should be checked for loss.
     */
    final boolean isTrackingRebuild()
    {
        return isTrackingRebuild;
    }

    /**
     * Insert frame into term buffer.
     *
     * @param termId         for the data packet to insert into the appropriate term.
     * @param termOffset     for the start of the packet in the term.
     * @param buffer         for the data packet to insert into the appropriate term.
     * @param length         of the data packet
     * @param transportIndex which the packet came from.
     * @param srcAddress     which the packet came from.
     * @return number of bytes applied as a result of this insertion.
     */
    int insertPacket(
        final int termId,
        final int termOffset,
        final UnsafeBuffer buffer,
        final int length,
        final int transportIndex,
        final InetSocketAddress srcAddress)
    {
        final boolean isHeartbeat = DataHeaderFlyweight.isHeartbeat(buffer, length);
        final long packetPosition = computePosition(termId, termOffset, positionBitsToShift, initialTermId);
        final long proposedPosition = isHeartbeat ? packetPosition : packetPosition + length;

        if (!isFlowControlUnderRun(packetPosition) && !isFlowControlOverRun(proposedPosition))
        {
            trackConnection(transportIndex, srcAddress, lastPacketTimestampNs);

            if (isHeartbeat)
            {
                if (DataHeaderFlyweight.isEndOfStream(buffer) && !isEndOfStream && allEos(transportIndex))
                {
                    LogBufferDescriptor.endOfStreamPosition(rawLog.metaData(), proposedPosition);
                    isEndOfStream = true;
                }

                heartbeatsReceived.incrementOrdered();
            }
            else
            {
                final UnsafeBuffer termBuffer = termBuffers[indexByPosition(packetPosition, positionBitsToShift)];
                TermRebuilder.insert(termBuffer, termOffset, buffer, length);
            }

            lastPacketTimestampNs = cachedNanoClock.nanoTime();
            hwmPosition.proposeMaxOrdered(proposedPosition);
        }

        return length;
    }

    /**
     * To be called from the {@link Receiver} to see if a image should be retained.
     *
     * @param nowNs current time to check against for activity.
     * @return true if the image should be retained otherwise false.
     */
    boolean hasActivityAndNotEndOfStream(final long nowNs)
    {
        boolean isActive = true;

        if (((lastPacketTimestampNs + imageLivenessTimeoutNs) - nowNs < 0) ||
            (isEndOfStream && rebuildPosition.getVolatile() >= hwmPosition.get()))
        {
            isActive = false;
        }

        return isActive;
    }

    /**
     * Called from the {@link Receiver} to send any pending Status Messages.
     *
     * @return number of work items processed.
     */
    int sendPendingStatusMessage()
    {
        int workCount = 0;

        if (ACTIVE == state)
        {
            final long changeNumber = endSmChange;

            if (changeNumber != lastSmChangeNumber)
            {
                final long smPosition = nextSmPosition;
                final int receiverWindowLength = nextSmReceiverWindowLength;

                UNSAFE.loadFence();

                if (changeNumber == beginSmChange)
                {
                    final int termId = computeTermIdFromPosition(smPosition, positionBitsToShift, initialTermId);
                    final int termOffset = (int)smPosition & termLengthMask;

                    channelEndpoint.sendStatusMessage(
                        imageConnections, sessionId, streamId, termId, termOffset, receiverWindowLength, (byte)0);

                    statusMessagesSent.incrementOrdered();

                    lastSmPosition = smPosition;
                    lastSmWindowLimit = smPosition + receiverWindowLength;
                    lastSmChangeNumber = changeNumber;
                }

                workCount = 1;
            }
        }

        return workCount;
    }

    /**
     * Called from the {@link Receiver} thread to processing any pending loss of packets.
     *
     * @return number of work items processed.
     */
    int processPendingLoss()
    {
        int workCount = 0;
        final long changeNumber = endLossChange;

        if (changeNumber != lastLossChangeNumber)
        {
            final int termId = lossTermId;
            final int termOffset = lossTermOffset;
            final int length = lossLength;

            UNSAFE.loadFence();

            if (changeNumber == beginLossChange)
            {
                if (isReliable)
                {
                    channelEndpoint.sendNakMessage(imageConnections, sessionId, streamId, termId, termOffset, length);
                    nakMessagesSent.incrementOrdered();
                }
                else
                {
                    final UnsafeBuffer termBuffer = termBuffers[indexByTerm(initialTermId, termId)];
                    if (tryFillGap(rawLog.metaData(), termBuffer, termId, termOffset, length))
                    {
                        lossGapFills.incrementOrdered();
                    }
                }

                lastLossChangeNumber = changeNumber;
            }

            workCount = 1;
        }

        return workCount;
    }

    /**
     * Called from the {@link Receiver} thread to check for initiating an RTT measurement.
     *
     * @param nowNs in nanoseconds
     * @return number of work items processed.
     */
    int initiateAnyRttMeasurements(final long nowNs)
    {
        int workCount = 0;

        if (congestionControl.shouldMeasureRtt(nowNs))
        {
            final long preciseTimeNs = nanoClock.nanoTime();

            channelEndpoint.sendRttMeasurement(imageConnections, sessionId, streamId, preciseTimeNs, 0, true);
            congestionControl.onRttMeasurementSent(preciseTimeNs);

            workCount = 1;
        }

        return workCount;
    }

    /**
     * Called from the {@link Receiver} upon receiving an RTT Measurement that is a reply.
     *
     * @param header         of the measurement message.
     * @param transportIndex that the RTT Measurement came in on.
     * @param srcAddress     from the sender requesting the measurement
     */
    void onRttMeasurement(
        final RttMeasurementFlyweight header,
        @SuppressWarnings("unused") final int transportIndex,
        final InetSocketAddress srcAddress)
    {
        final long nowNs = nanoClock.nanoTime();
        final long rttInNs = nowNs - header.echoTimestampNs() - header.receptionDelta();

        congestionControl.onRttMeasurement(nowNs, rttInNs, srcAddress);
    }

    /**
     * Is the image in a state to accept new subscriptions?
     *
     * @return true if accepting new subscriptions.
     */
    boolean isAcceptingSubscriptions()
    {
        return subscriberPositions.length > 0 && (state == ACTIVE || state == INIT);
    }

    /**
     * The position up to which the current stream rebuild is complete for reception.
     *
     * @return the position up to which the current stream rebuild is complete for reception.
     */
    long rebuildPosition()
    {
        return rebuildPosition.get();
    }

    /**
     * {@inheritDoc}
     */
    public void onTimeEvent(final long timeNs, final long timesMs, final DriverConductor conductor)
    {
        switch (state)
        {
            case ACTIVE:
                checkUntetheredSubscriptions(timeNs, conductor);
                break;

            case INACTIVE:
                if (isDrained())
                {
                    state = State.LINGER;
                    timeOfLastStateChangeNs = timeNs;
                    conductor.transitionToLinger(this);
                }
                isTrackingRebuild = false;
                break;

            case LINGER:
                if ((timeOfLastStateChangeNs + imageLivenessTimeoutNs) - timeNs < 0)
                {
                    state = State.DONE;
                    conductor.cleanupImage(this);
                }
                break;
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasReachedEndOfLife()
    {
        return State.DONE == state;
    }

    private boolean isDrained()
    {
        final long rebuildPosition = this.rebuildPosition.get();

        for (final ReadablePosition subscriberPosition : subscriberPositions)
        {
            if (subscriberPosition.getVolatile() < rebuildPosition)
            {
                return false;
            }
        }

        return true;
    }

    private boolean isFlowControlUnderRun(final long packetPosition)
    {
        final boolean isFlowControlUnderRun = packetPosition < lastSmPosition;

        if (isFlowControlUnderRun)
        {
            flowControlUnderRuns.incrementOrdered();
        }

        return isFlowControlUnderRun;
    }

    private boolean isFlowControlOverRun(final long proposedPosition)
    {
        final boolean isFlowControlOverRun = proposedPosition > lastSmWindowLimit;

        if (isFlowControlOverRun)
        {
            flowControlOverRuns.incrementOrdered();
        }

        return isFlowControlOverRun;
    }

    private void cleanBufferTo(final long newCleanPosition)
    {
        final long cleanPosition = this.cleanPosition;
        final int bytesForCleaning = (int)(newCleanPosition - cleanPosition);
        final UnsafeBuffer dirtyTerm = termBuffers[indexByPosition(cleanPosition, positionBitsToShift)];
        final int termOffset = (int)cleanPosition & termLengthMask;
        final int length = Math.min(bytesForCleaning, dirtyTerm.capacity() - termOffset);

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

    private void trackConnection(final int transportIndex, final InetSocketAddress srcAddress, final long nowNs)
    {
        imageConnections = ArrayUtil.ensureCapacity(imageConnections, transportIndex + 1);
        ImageConnection imageConnection = imageConnections[transportIndex];

        if (null == imageConnection)
        {
            imageConnection = new ImageConnection(nowNs, srcAddress);
            imageConnections[transportIndex] = imageConnection;
        }

        imageConnection.timeOfLastFrameNs = nowNs;
    }

    private boolean allEos(final int transportIndex)
    {
        imageConnections[transportIndex].isEos = true;

        for (int i = 0, length = imageConnections.length; i < length; i++)
        {
            final ImageConnection imageConnection = imageConnections[i];

            if (null != imageConnection && !imageConnection.isEos)
            {
                return false;
            }
            else if (null == imageConnection && channelEndpoint.hasDestination(i))
            {
                return false;
            }
        }

        return true;
    }

    private void state(final State state)
    {
        timeOfLastStateChangeNs = cachedNanoClock.nanoTime();
        this.state = state;
    }

    private void scheduleStatusMessage(final long nowNs, final long smPosition, final int receiverWindowLength)
    {
        final long changeNumber = beginSmChange + 1;
        beginSmChange = changeNumber;

        nextSmPosition = smPosition;
        nextSmReceiverWindowLength = receiverWindowLength;

        endSmChange = changeNumber;

        timeOfLastStatusMessageScheduleNs = nowNs;
    }

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

        long maxConsumerPosition = 0;
        for (final ReadablePosition subscriberPosition : subscriberPositions)
        {
            final long position = subscriberPosition.getVolatile();
            if (position > maxConsumerPosition)
            {
                maxConsumerPosition = position;
            }
        }

        final int windowLength = nextSmReceiverWindowLength;
        final long untetheredWindowLimit = (maxConsumerPosition - windowLength) + (windowLength >> 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(correlationId, untethered.subscriptionLink);
                        untethered.state = UntetheredSubscription.LINGER;
                        untethered.timeOfLastUpdateNs = nowNs;
                    }
                    break;

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

                case UntetheredSubscription.RESTING:
                    if ((untethered.timeOfLastUpdateNs + untetheredRestingTimeoutNs) - nowNs <= 0)
                    {
                        subscriberPositions = ArrayUtil.add(subscriberPositions, untethered.position);
                        conductor.notifyAvailableImageLink(
                            correlationId,
                            sessionId,
                            untethered.subscriptionLink,
                            untethered.position.id(),
                            rebuildPosition.get(),
                            rawLog.fileName(),
                            Configuration.sourceIdentity(sourceAddress));
                        untethered.state = UntetheredSubscription.ACTIVE;
                        untethered.timeOfLastUpdateNs = nowNs;
                    }
                    break;
            }
        }
    }

    private ReadablePosition[] positionArray(final ArrayList<SubscriberPosition> subscriberPositions, final long nowNs)
    {
        final int size = subscriberPositions.size();
        final ReadablePosition[] positions = new ReadablePosition[subscriberPositions.size()];

        for (int i = 0; i < size; i++)
        {
            final SubscriberPosition subscriberPosition = subscriberPositions.get(i);
            positions[i] = subscriberPosition.position();

            if (!subscriberPosition.subscription().isTether())
            {
                untetheredSubscriptions.add(new UntetheredSubscription(
                    subscriberPosition.subscription(), subscriberPosition.position(), nowNs));
            }
        }

        return positions;
    }
}


/*
 *  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.cluster;

import io.aeron.CncFileDescriptor;
import io.aeron.CommonContext;
import io.aeron.cluster.client.ClusterException;
import io.aeron.exceptions.AeronException;
import org.agrona.DirectBuffer;
import org.agrona.IoUtil;
import org.agrona.concurrent.AtomicBuffer;
import org.agrona.concurrent.status.AtomicCounter;
import org.agrona.concurrent.status.CountersReader;

import java.io.File;
import java.nio.MappedByteBuffer;
import java.nio.charset.StandardCharsets;

import static io.aeron.CncFileDescriptor.*;
import static org.agrona.concurrent.status.CountersReader.RECORD_ALLOCATED;
import static org.agrona.concurrent.status.CountersReader.TYPE_ID_OFFSET;

/**
 * Toggle control {@link ToggleState}s for a cluster node such as {@link ToggleState#SUSPEND} or {@link ToggleState#RESUME}.
 */
public class ClusterControl
{
    /**
     * Toggle states for controlling the cluster node once it has entered the active state after initialising.
     * The toggle can only we switched into a new state from {@link #NEUTRAL} and will be reset by the
     * {@link io.aeron.cluster.ConsensusModule} once the triggered action is complete.
     */
    public enum ToggleState
    {
        /**
         * Neutral state ready to accept a new action.
         */
        NEUTRAL(0),

        /**
         * Suspend processing of ingress and timers.
         */
        SUSPEND(1),

        /**
         * Resume processing of ingress and timers.
         */
        RESUME(2),

        /**
         * Take a snapshot of cluster state.
         */
        SNAPSHOT(3),

        /**
         * Shut down the cluster in an orderly fashion by taking a snapshot first then terminating.
         */
        SHUTDOWN(4),

        /**
         * Abort processing and terminate the cluster without taking a snapshot.
         */
        ABORT(5);

        private final int code;

        private static final ToggleState[] STATES;
        static
        {
            final ToggleState[] toggleStates = values();
            STATES = new ToggleState[toggleStates.length];
            for (final ToggleState toggleState : toggleStates)
            {
                STATES[toggleState.code()] = toggleState;
            }
        }

        ToggleState(final int code)
        {
            this.code = code;
        }

        /**
         * Code to be used as the indicator in the control toggle counter.
         *
         * @return code to be used as the indicator in the control toggle counter.
         */
        public final int code()
        {
            return code;
        }

        /**
         * Toggle the control counter to trigger the requested {@link ToggleState}.
         * <p>
         * This action is thread safe and will only succeed if the toggle is in the {@link ToggleState#NEUTRAL} state.
         *
         * @param controlToggle to change to the trigger state.
         * @return true if the counter toggles or false if it is in a state other than {@link ToggleState#NEUTRAL}.
         */
        public final boolean toggle(final AtomicCounter controlToggle)
        {
            return controlToggle.compareAndSet(NEUTRAL.code(), code());
        }

        /**
         * Reset the toggle to the {@link #NEUTRAL} state.
         *
         * @param controlToggle to be reset.
         */
        public static void reset(final AtomicCounter controlToggle)
        {
            controlToggle.set(NEUTRAL.code());
        }

        /**
         * Get the {@link ToggleState} for a given control toggle.
         *
         * @param controlToggle to get the current state for.
         * @return the state for the current control toggle.
         * @throws ClusterException if the counter is not one of the valid values.
         */
        public static ToggleState get(final AtomicCounter controlToggle)
        {
            final long toggleValue = controlToggle.get();

            if (toggleValue < 0 || toggleValue > (STATES.length - 1))
            {
                throw new ClusterException("invalid toggle value: " + toggleValue);
            }

            return STATES[(int)toggleValue];
        }
    }

    /**
     * Counter type id for the control toggle.
     */
    public static final int CONTROL_TOGGLE_TYPE_ID = 202;

    /**
     * Map a {@link CountersReader} over the default location CnC file.
     *
     * @return a {@link CountersReader} over the default location CnC file.
     */
    public static CountersReader mapCounters()
    {
        return mapCounters(CommonContext.newDefaultCncFile());
    }

    /**
     * Map a {@link CountersReader} over the provided filename for the CnC file.
     *
     * @param filename for the CnC file.
     * @return a {@link CountersReader} over the provided CnC file.
     */
    public static CountersReader mapCounters(final String filename)
    {
        return mapCounters(new File(filename));
    }

    /**
     * Map a {@link CountersReader} over the provided {@link File} for the CnC file.
     *
     * @param cncFile for the counters.
     * @return a {@link CountersReader} over the provided CnC file.
     */
    public static CountersReader mapCounters(final File cncFile)
    {
        final MappedByteBuffer cncByteBuffer = IoUtil.mapExistingFile(cncFile, "cnc");
        final DirectBuffer cncMetaData = createMetaDataBuffer(cncByteBuffer);
        final int cncVersion = cncMetaData.getInt(cncVersionOffset(0));

        if (CncFileDescriptor.CNC_VERSION != cncVersion)
        {
            throw new AeronException(
                "Aeron CnC version does not match: version=" + cncVersion + " required=" + CNC_VERSION);
        }

        return new CountersReader(
            createCountersMetaDataBuffer(cncByteBuffer, cncMetaData),
            createCountersValuesBuffer(cncByteBuffer, cncMetaData),
            StandardCharsets.US_ASCII);
    }

    /**
     * Find the control toggle counter or return null if not found.
     *
     * @param counters to search for the control toggle.
     * @return the control toggle counter or return null if not found.
     */
    public static AtomicCounter findControlToggle(final CountersReader counters)
    {
        final AtomicBuffer buffer = counters.metaDataBuffer();

        for (int i = 0, size = counters.maxCounterId(); i < size; i++)
        {
            final int recordOffset = CountersReader.metaDataOffset(i);

            if (counters.getCounterState(i) == RECORD_ALLOCATED &&
                buffer.getInt(recordOffset + TYPE_ID_OFFSET) == CONTROL_TOGGLE_TYPE_ID)
            {
                return new AtomicCounter(counters.valuesBuffer(), i, null);
            }
        }

        return null;
    }

    public static void main(final String[] args)
    {
        checkUsage(args);

        final ToggleState toggleState = ToggleState.valueOf(args[0].toUpperCase());

        final File cncFile = CommonContext.newDefaultCncFile();
        System.out.println("Command `n Control file " + cncFile);

        final CountersReader countersReader = mapCounters(cncFile);
        final AtomicCounter controlToggle = findControlToggle(countersReader);

        if (null == controlToggle)
        {
            System.out.println("Failed to find control toggle");
            System.exit(0);
        }

        if (toggleState.toggle(controlToggle))
        {
            System.out.println(toggleState + " toggled successfully");
        }
        else
        {
            System.out.println(toggleState + " did NOT toggle");
        }
    }

    private static void checkUsage(final String[] args)
    {
        if (1 != args.length)
        {
            System.out.format("Usage: [-Daeron.dir=<directory containing CnC file>] " +
                ClusterControl.class.getSimpleName() + " <action>%n");

            System.exit(0);
        }
    }
}


/*
 * 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;

import io.aeron.logbuffer.LogBufferDescriptor;
import org.agrona.AsciiEncoding;
import org.agrona.collections.ArrayUtil;
import org.agrona.collections.Object2ObjectHashMap;

import java.util.*;

import static io.aeron.CommonContext.*;
import static io.aeron.logbuffer.FrameDescriptor.FRAME_ALIGNMENT;

/**
 * Parser for Aeron channel URIs. The format is:
 *

 * aeron-uri = "aeron:" media [ "?" param *( "|" param ) ]
 * media     = *( "[^?:]" )
 * param     = key "=" value
 * key       = *( "[^=]" )
 * value     = *( "[^|]" )
 *

 *

 * Multiple params with the same key are allowed, the last value specified takes precedence.
 * @see ChannelUriStringBuilder
 */
public class ChannelUri
{
    private enum State
    {
        MEDIA, PARAMS_KEY, PARAMS_VALUE
    }

    /**
     * URI Scheme for Aeron channels.
     */
    public static final String AERON_SCHEME = "aeron";

    /**
     * Qualifier for spy subscriptions which spy on outgoing network destined traffic efficiently.
     */
    public static final String SPY_QUALIFIER = "aeron-spy";

    public static final long INVALID_TAG = Aeron.NULL_VALUE;

    private static final int CHANNEL_TAG_INDEX = 0;
    private static final int ENTITY_TAG_INDEX = 1;

    private static final String AERON_PREFIX = AERON_SCHEME + ":";

    private String prefix;
    private String media;
    private final Map<String, String> params;
    private final String[] tags;

    /**
     * Construct with the components provided to avoid parsing.
     *
     * @param prefix empty if no prefix is required otherwise expected to be 'aeron-spy'
     * @param media  for the channel which is typically "udp" or "ipc".
     * @param params for the query string as key value pairs.
     */
    public ChannelUri(final String prefix, final String media, final Map<String, String> params)
    {
        this.prefix = prefix;
        this.media = media;
        this.params = params;
        this.tags = splitTags(params.get(TAGS_PARAM_NAME));
    }

    /**
     * Construct with the components provided to avoid parsing.
     *
     * @param media  for the channel which is typically "udp" or "ipc".
     * @param params for the query string as key value pairs.
     */
    public ChannelUri(final String media, final Map<String, String> params)
    {
        this("", media, params);
    }

    /**
     * The prefix for the channel.
     *
     * @return the prefix for the channel.
     */
    public String prefix()
    {
        return prefix;
    }

    /**
     * Change the prefix from what has been parsed.
     *
     * @param prefix to replace the existing prefix.
     * @return this for a fluent API.
     */
    public ChannelUri prefix(final String prefix)
    {
        this.prefix = prefix;
        return this;
    }

    /**
     * The media over which the channel operates.
     *
     * @return the media over which the channel operates.
     */
    public String media()
    {
        return media;
    }

    /**
     * Set the media over which the channel operates.
     *
     * @param media to replace the parsed value.
     * @return this for a fluent API.
     */
    public ChannelUri media(final String media)
    {
        this.media = media;
        return this;
    }

    /**
     * The scheme for the URI. Must be "aeron".
     *
     * @return the scheme for the URI.
     */
    public String scheme()
    {
        return AERON_SCHEME;
    }

    /**
     * Get a value for a given parameter key.
     *
     * @param key to lookup.
     * @return the value if set for the key otherwise null.
     */
    public String get(final String key)
    {
        return params.get(key);
    }

    /**
     * Get the value for a given parameter key or the default value provided if the key does not exist.
     *
     * @param key          to lookup.
     * @param defaultValue to be returned if no key match is found.
     * @return the value if set for the key otherwise the default value provided.
     */
    public String get(final String key, final String defaultValue)
    {
        final String value = params.get(key);
        if (null != value)
        {
            return value;
        }

        return defaultValue;
    }

    /**
     * Put a key and value pair in the map of params.
     *
     * @param key   of the param to be put.
     * @param value of the param to be put.
     * @return the existing value otherwise null.
     */
    public String put(final String key, final String value)
    {
        return params.put(key, value);
    }

    /**
     * Remove a key pair in the map of params.
     *
     * @param key of the param to be removed.
     * @return the previous value of the param or null.
     */
    public String remove(final String key)
    {
        return params.remove(key);
    }

    /**
     * Does the URI contain a value for the given key.
     *
     * @param key to be lookup.
     * @return true if the key has a value otherwise false.
     */
    public boolean containsKey(final String key)
    {
        return params.containsKey(key);
    }

    /**
     * Get the channel tag, if it exists, that refers to an another channel.
     *
     * @return channel tag if it exists or null if not in this URI.
     * @see CommonContext#TAGS_PARAM_NAME
     * @see CommonContext#TAG_PREFIX
     */
    public String channelTag()
    {
        return (null != tags && tags.length > CHANNEL_TAG_INDEX) ? tags[CHANNEL_TAG_INDEX] : null;
    }

    /**
     * Get the entity tag, if it exists, that refers to an entity such as subscription or publication.
     *
     * @return entity tag if it exists or null if not in this URI.
     * @see CommonContext#TAGS_PARAM_NAME
     * @see CommonContext#TAG_PREFIX
     */
    public String entityTag()
    {
        return (null != tags && tags.length > ENTITY_TAG_INDEX) ? tags[ENTITY_TAG_INDEX] : null;
    }

    /**
     * Generate a String representation of the URI that is valid for an Aeron channel.
     *
     * @return a String representation of the URI that is valid for an Aeron channel.
     */
    public String toString()
    {
        final StringBuilder sb;
        if (prefix == null || "".equals(prefix))
        {
            sb = new StringBuilder((params.size() * 20) + 10);
        }
        else
        {
            sb = new StringBuilder((params.size() * 20) + 20);
            sb.append(prefix);
            if (!prefix.endsWith(":"))
            {
                sb.append(':');
            }
        }

        sb.append(AERON_PREFIX).append(media);

        if (params.size() > 0)
        {
            sb.append('?');

            for (final Map.Entry<String, String> entry : params.entrySet())
            {
                sb.append(entry.getKey()).append('=').append(entry.getValue()).append('|');
            }

            sb.setLength(sb.length() - 1);
        }

        return sb.toString();
    }

    /**
     * Initialise a channel for restarting a publication at a given position.
     *
     * @param position      at which the publication should be started.
     * @param initialTermId what which the stream would start.
     * @param termLength    for the stream.
     */
    public void initialPosition(final long position, final int initialTermId, final int termLength)
    {
        if (position < 0 || 0 != (position & (FRAME_ALIGNMENT - 1)))
        {
            throw new IllegalArgumentException("invalid position: " + position);
        }

        final int bitsToShift = LogBufferDescriptor.positionBitsToShift(termLength);
        final int termId = LogBufferDescriptor.computeTermIdFromPosition(position, bitsToShift, initialTermId);
        final int termOffset = (int)(position & (termLength - 1));

        put(INITIAL_TERM_ID_PARAM_NAME, Integer.toString(initialTermId));
        put(TERM_ID_PARAM_NAME, Integer.toString(termId));
        put(TERM_OFFSET_PARAM_NAME, Integer.toString(termOffset));
        put(TERM_LENGTH_PARAM_NAME, Integer.toString(termLength));
    }

    /**
     * Parse a {@link CharSequence} which contains an Aeron URI.
     *
     * @param cs to be parsed.
     * @return a new {@link ChannelUri} representing the URI string.
     */
    public static ChannelUri parse(final CharSequence cs)
    {
        int position = 0;
        final String prefix;
        if (startsWith(cs, 0, SPY_PREFIX))
        {
            prefix = SPY_QUALIFIER;
            position = SPY_PREFIX.length();
        }
        else
        {
            prefix = "";
        }

        if (!startsWith(cs, position, AERON_PREFIX))
        {
            throw new IllegalArgumentException("Aeron URIs must start with 'aeron:', found: '" + cs + "'");
        }
        else
        {
            position += AERON_PREFIX.length();
        }

        final StringBuilder builder = new StringBuilder();
        final Map<String, String> params = new Object2ObjectHashMap<>();
        String media = null;
        String key = null;

        State state = State.MEDIA;
        for (int i = position; i < cs.length(); i++)
        {
            final char c = cs.charAt(i);

            switch (state)
            {
                case MEDIA:
                    switch (c)
                    {
                        case '?':
                            media = builder.toString();
                            builder.setLength(0);
                            state = State.PARAMS_KEY;
                            break;

                        case ':':
                            throw new IllegalArgumentException("encountered ':' within media definition");

                        default:
                            builder.append(c);
                    }
                    break;

                case PARAMS_KEY:
                    if (c == '=')
                    {
                        key = builder.toString();
                        builder.setLength(0);
                        state = State.PARAMS_VALUE;
                    }
                    else
                    {
                        builder.append(c);
                    }
                    break;

                case PARAMS_VALUE:
                    if (c == '|')
                    {
                        params.put(key, builder.toString());
                        builder.setLength(0);
                        state = State.PARAMS_KEY;
                    }
                    else
                    {
                        builder.append(c);
                    }
                    break;

                default:
                    throw new IllegalStateException("unexpected state=" + state);
            }
        }

        switch (state)
        {
            case MEDIA:
                media = builder.toString();
                break;

            case PARAMS_VALUE:
                params.put(key, builder.toString());
                break;

            default:
                throw new IllegalArgumentException("no more input found, state=" + state);
        }

        return new ChannelUri(prefix, media, params);
    }

    /**
     * Add a sessionId to a given channel.
     *
     * @param channel   to add sessionId to.
     * @param sessionId to add to channel.
     * @return new string that represents channel with sessionId added.
     */
    public static String addSessionId(final String channel, final int sessionId)
    {
        final ChannelUri channelUri = ChannelUri.parse(channel);
        channelUri.put(CommonContext.SESSION_ID_PARAM_NAME, Integer.toString(sessionId));

        return channelUri.toString();
    }

    /**
     * Is the param value tagged? (starts with the "tag:" prefix)
     *
     * @param paramValue to check if tagged.
     * @return true if tagged or false if not.
     * @see CommonContext#TAGS_PARAM_NAME
     * @see CommonContext#TAG_PREFIX
     */
    public static boolean isTagged(final String paramValue)
    {
        return startsWith(paramValue, 0, TAG_PREFIX);
    }

    /**
     * Get the value of the tag from a given parameter value.
     *
     * @param paramValue to extract the tag value from.
     * @return the value of the tag or {@link #INVALID_TAG} if not tagged.
     * @see CommonContext#TAGS_PARAM_NAME
     * @see CommonContext#TAG_PREFIX
     */
    public static long getTag(final String paramValue)
    {
        return isTagged(paramValue) ?
            AsciiEncoding.parseLongAscii(paramValue, 4, paramValue.length() - 4) : INVALID_TAG;
    }

    private static boolean startsWith(final CharSequence input, final int position, final CharSequence prefix)
    {
        if ((input.length() - position) < prefix.length())
        {
            return false;
        }

        for (int i = 0; i < prefix.length(); i++)
        {
            if (input.charAt(position + i) != prefix.charAt(i))
            {
                return false;
            }
        }

        return true;
    }

    private static String[] splitTags(final String tagsValue)
    {
        String[] tags = ArrayUtil.EMPTY_STRING_ARRAY;

        if (null != tagsValue)
        {
            final int tagCount = countTags(tagsValue);
            if (tagCount == 1)
            {
                tags = new String[]{ tagsValue };
            }
            else
            {
                int tagStartPosition = 0;
                int tagIndex = 0;
                tags = new String[tagCount];

                for (int i = 0, length = tagsValue.length(); i < length; i++)
                {
                    if (tagsValue.charAt(i) == ',')
                    {
                        tags[tagIndex++] = tagsValue.substring(tagStartPosition, i);
                        tagStartPosition = i + 1;

                        if (tagIndex >= (tagCount - 1))
                        {
                            tags[tagIndex] = tagsValue.substring(tagStartPosition, length);
                        }
                    }
                }
            }
        }

        return tags;
    }

    private static int countTags(final String tags)
    {
        int count = 1;

        for (int i = 0, length = tags.length(); i < length; i++)
        {
            if (tags.charAt(i) == ',')
            {
                ++count;
            }
        }

        return count;
    }
}