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

import io.aeron.*;
import io.aeron.cluster.codecs.*;
import io.aeron.exceptions.ConfigurationException;
import io.aeron.exceptions.TimeoutException;
import io.aeron.logbuffer.BufferClaim;
import io.aeron.logbuffer.ControlledFragmentHandler;
import io.aeron.logbuffer.Header;
import io.aeron.security.AuthenticationException;
import io.aeron.security.CredentialsSupplier;
import io.aeron.security.NullCredentialsSupplier;
import org.agrona.*;
import org.agrona.collections.Int2ObjectHashMap;
import org.agrona.concurrent.*;

import java.util.concurrent.TimeUnit;

import static org.agrona.SystemUtil.getDurationInNanos;

/**
 * Client for interacting with an Aeron Cluster.
 *

 * A client will attempt to open a session and then offer ingress messages which are replicated to clustered services
 * for reliability. If the clustered service responds then response messages and events come back via the egress stream.
 *

 * Note: Instances of this class are not threadsafe.
 */
@SuppressWarnings("unused")
public final class AeronCluster implements AutoCloseable
{
    public static final int INGRESS_HEADER_LENGTH =
        MessageHeaderEncoder.ENCODED_LENGTH + IngressMessageHeaderEncoder.BLOCK_LENGTH;

    public static final int EGRESS_HEADER_LENGTH =
        MessageHeaderEncoder.ENCODED_LENGTH + EgressMessageHeaderEncoder.BLOCK_LENGTH;

    static final int SEND_ATTEMPTS = 3;
    static final int FRAGMENT_LIMIT = 10;

    private final long clusterSessionId;
    private long leadershipTermId;
    private int leaderMemberId;
    private final Context ctx;
    private final Subscription subscription;
    private Publication publication;
    private final IdleStrategy idleStrategy;
    private final BufferClaim bufferClaim = new BufferClaim();
    private final UnsafeBuffer headerBuffer = new UnsafeBuffer(new byte[INGRESS_HEADER_LENGTH]);
    private final DirectBufferVector headerVector = new DirectBufferVector(headerBuffer, 0, INGRESS_HEADER_LENGTH);
    private final UnsafeBuffer keepaliveMsgBuffer;
    private final MessageHeaderEncoder messageHeaderEncoder;
    private final IngressMessageHeaderEncoder ingressMessageHeaderEncoder = new IngressMessageHeaderEncoder();
    private final SessionKeepAliveEncoder sessionKeepAliveEncoder = new SessionKeepAliveEncoder();
    private final MessageHeaderDecoder messageHeaderDecoder = new MessageHeaderDecoder();
    private final EgressMessageHeaderDecoder egressMessageHeaderDecoder = new EgressMessageHeaderDecoder();
    private final NewLeaderEventDecoder newLeaderEventDecoder = new NewLeaderEventDecoder();
    private final SessionEventDecoder sessionEventDecoder = new SessionEventDecoder();
    private final FragmentAssembler fragmentAssembler;
    private final EgressListener egressListener;
    private final ControlledFragmentAssembler controlledFragmentAssembler;
    private final ControlledEgressListener controlledEgressListener;
    private Int2ObjectHashMap<MemberEndpoint> endpointByMemberIdMap;

    /**
     * Connect to the cluster using default configuration.
     *
     * @return allocated cluster client if the connection is successful.
     */
    public static AeronCluster connect()
    {
        return connect(new Context());
    }

    /**
     * Connect to the cluster providing {@link Context} for configuration.
     *
     * @param ctx for configuration.
     * @return allocated cluster client if the connection is successful.
     */
    public static AeronCluster connect(final AeronCluster.Context ctx)
    {
        Subscription subscription = null;
        AsyncConnect asyncConnect = null;
        try
        {
            ctx.conclude();

            final Aeron aeron = ctx.aeron();
            final long deadlineNs = aeron.context().nanoClock().nanoTime() + ctx.messageTimeoutNs();
            subscription = aeron.addSubscription(ctx.egressChannel(), ctx.egressStreamId());

            final IdleStrategy idleStrategy = ctx.idleStrategy();
            asyncConnect = new AsyncConnect(ctx, subscription, deadlineNs);
            final AgentInvoker aeronClientInvoker = aeron.conductorAgentInvoker();

            AeronCluster aeronCluster;
            while (null == (aeronCluster = asyncConnect.poll()))
            {
                if (null != aeronClientInvoker)
                {
                    aeronClientInvoker.invoke();
                }
                idleStrategy.idle();
            }

            return aeronCluster;
        }
        catch (final Exception ex)
        {
            if (!ctx.ownsAeronClient())
            {
                CloseHelper.close(subscription);
                CloseHelper.close(asyncConnect);
            }

            ctx.close();

            throw ex;
        }
    }

    /**
     * Begin an attempt at creating a connection which can be completed by calling {@link AsyncConnect#poll()} until
     * it returns the client, before complete it will return null.
     *
     * @return the {@link AsyncConnect} that can be polled for completion.
     */
    public static AsyncConnect asyncConnect()
    {
        return asyncConnect(new Context());
    }

    /**
     * Begin an attempt at creating a connection which can be completed by calling {@link AsyncConnect#poll()} until
     * it returns the client, before complete it will return null.
     *
     * @param ctx for the cluster.
     * @return the {@link AsyncConnect} that can be polled for completion.
     */
    public static AsyncConnect asyncConnect(final Context ctx)
    {
        Subscription subscription = null;
        try
        {
            ctx.conclude();

            final long deadlineNs = ctx.aeron().context().nanoClock().nanoTime() + ctx.messageTimeoutNs();
            subscription = ctx.aeron().addSubscription(ctx.egressChannel(), ctx.egressStreamId());

            return new AsyncConnect(ctx, subscription, deadlineNs);
        }
        catch (final Exception ex)
        {
            if (!ctx.ownsAeronClient())
            {
                CloseHelper.quietClose(subscription);
            }

            ctx.close();

            throw ex;
        }
    }

    private AeronCluster(
        final Context ctx,
        final MessageHeaderEncoder messageHeaderEncoder,
        final Publication publication,
        final Subscription subscription,
        final Int2ObjectHashMap<MemberEndpoint> endpointByMemberIdMap,
        final long clusterSessionId,
        final long leadershipTermId,
        final int leaderMemberId)
    {
        this.ctx = ctx;
        this.messageHeaderEncoder = messageHeaderEncoder;
        this.subscription = subscription;
        this.endpointByMemberIdMap = endpointByMemberIdMap;
        this.clusterSessionId = clusterSessionId;
        this.leadershipTermId = leadershipTermId;
        this.leaderMemberId = leaderMemberId;
        this.publication = publication;

        this.idleStrategy = ctx.idleStrategy();
        this.egressListener = ctx.egressListener();
        this.fragmentAssembler = new FragmentAssembler(this::onFragment, 0, ctx.isDirectAssemblers());
        this.controlledEgressListener = ctx.controlledEgressListener();
        this.controlledFragmentAssembler = new ControlledFragmentAssembler(
            this::onControlledFragment, 0, ctx.isDirectAssemblers());

        ingressMessageHeaderEncoder
            .wrapAndApplyHeader(headerBuffer, 0, messageHeaderEncoder)
            .clusterSessionId(clusterSessionId)
            .leadershipTermId(leadershipTermId);

        keepaliveMsgBuffer = new UnsafeBuffer(new byte[
            MessageHeaderEncoder.ENCODED_LENGTH + SessionKeepAliveEncoder.BLOCK_LENGTH]);

        sessionKeepAliveEncoder
            .wrapAndApplyHeader(keepaliveMsgBuffer, 0, messageHeaderEncoder)
            .leadershipTermId(leadershipTermId)
            .clusterSessionId(clusterSessionId);
    }

    /**
     * Close session and release associated resources.
     */
    public void close()
    {
        if (null != publication && publication.isConnected())
        {
            closeSession();
        }

        if (!ctx.ownsAeronClient())
        {
            CloseHelper.close(subscription);
            CloseHelper.close(publication);
        }

        ctx.close();
    }

    /**
     * Get the context used to launch this cluster client.
     *
     * @return the context used to launch this cluster client.
     */
    public Context context()
    {
        return ctx;
    }

    /**
     * Cluster session id for the session that was opened as the result of a successful connect.
     *
     * @return session id for the session that was opened as the result of a successful connect.
     */
    public long clusterSessionId()
    {
        return clusterSessionId;
    }

    /**
     * Leadership term identity for the cluster. Advances with changing leadership.
     *
     * @return leadership term identity for the cluster.
     */
    public long leadershipTermId()
    {
        return leadershipTermId;
    }

    /**
     * Get the current leader member id for the cluster.
     *
     * @return the current leader member id for the cluster.
     */
    public int leaderMemberId()
    {
        return leaderMemberId;
    }

    /**
     * Get the raw {@link Publication} for sending to the cluster.
     *

     * This can be wrapped with a {@link IngressSessionDecorator} for pre-pending the cluster session header to
     * messages.
     * {@link io.aeron.cluster.codecs.SessionHeaderEncoder} or equivalent should be used to raw access.
     *
     * @return the raw {@link Publication} for connecting to the cluster.
     */
    public Publication ingressPublication()
    {
        return publication;
    }

    /**
     * Get the raw {@link Subscription} for receiving from the cluster.
     *

     * The can be wrapped with a {@link EgressAdapter} for dispatching events from the cluster.
     *
     * @return the raw {@link Subscription} for receiving from the cluster.
     */
    public Subscription egressSubscription()
    {
        return subscription;
    }

    /**
     * Non-blocking publish of a partial buffer containing a message plus session header to a cluster.
     *

     * This version of the method will set the timestamp value in the header to zero.
     *
     * @param buffer containing message.
     * @param offset offset in the buffer at which the encoded message begins.
     * @param length in bytes of the encoded message.
     * @return the same as {@link Publication#offer(DirectBuffer, int, int)}.
     */
    public long offer(final DirectBuffer buffer, final int offset, final int length)
    {
        return publication.offer(headerBuffer, 0, INGRESS_HEADER_LENGTH, buffer, offset, length, null);
    }

    /**
     * Non-blocking publish by gathering buffer vectors into a message. The first vector will be replaced by the cluster
     * ingress header so must be left unused.
     *
     * @param vectors which make up the message.
     * @return the same as {@link Publication#offer(DirectBufferVector[])}.
     * @see Publication#offer(DirectBufferVector[])
     */
    public long offer(final DirectBufferVector[] vectors)
    {
        if (headerVector != vectors[0])
        {
            vectors[0] = headerVector;
        }

        return publication.offer(vectors, null);
    }

    /**
     * Send a keep alive message to the cluster to keep this session open.
     *

     * Note: keepalives can fail during a leadership transition. The consumer should continue to call
     * {@link #pollEgress()} to ensure a connection to the new leader is established.
     *
     * @return true if successfully sent otherwise false.
     */
    public boolean sendKeepAlive()
    {
        idleStrategy.reset();
        int attempts = SEND_ATTEMPTS;

        while (true)
        {
            final long result = publication.offer(keepaliveMsgBuffer, 0, keepaliveMsgBuffer.capacity(), null);
            if (result > 0)
            {
                return true;
            }

            if (result == Publication.NOT_CONNECTED || result == Publication.CLOSED)
            {
                return false;
            }

            if (result == Publication.MAX_POSITION_EXCEEDED)
            {
                throw new ClusterException("unexpected publication state: " + result);
            }

            if (--attempts <= 0)
            {
                break;
            }

            idleStrategy.idle();
        }

        return false;
    }

    /**
     * Poll the {@link #egressSubscription()} for session messages which are dispatched to
     * {@link Context#egressListener()}.
     *

     * Note: if {@link Context#egressListener()} is not set then a {@link ConfigurationException} could result.
     *
     * @return the number of fragments processed.
     */
    public int pollEgress()
    {
        return subscription.poll(fragmentAssembler, FRAGMENT_LIMIT);
    }

    /**
     * Poll the {@link #egressSubscription()} for session messages which are dispatched to
     * {@link Context#controlledEgressListener()}.
     *

     * Note: if {@link Context#controlledEgressListener()} is not set then a {@link ConfigurationException}
     * could result.
     *
     * @return the number of fragments processed.
     */
    public int controlledPollEgress()
    {
        return subscription.controlledPoll(controlledFragmentAssembler, FRAGMENT_LIMIT);
    }

    /**
     * To be called when a new leader event is delivered. This method needs to be called when using the
     * {@link EgressAdapter} or {@link EgressPoller} rather than {@link #pollEgress()} method.
     *
     * @param clusterSessionId which must match {@link #clusterSessionId()}.
     * @param leadershipTermId that identifies the term for which the new leader has been elected.
     * @param leaderMemberId   which has become the new leader.
     * @param memberEndpoints  comma separated list of cluster members endpoints to connect to with the leader first.
     */
    public void onNewLeader(
        final long clusterSessionId,
        final long leadershipTermId,
        final int leaderMemberId,
        final String memberEndpoints)
    {
        if (clusterSessionId != this.clusterSessionId)
        {
            throw new ClusterException(
                "invalid clusterSessionId=" + clusterSessionId + " expected " + this.clusterSessionId);
        }

        this.leadershipTermId = leadershipTermId;
        this.leaderMemberId = leaderMemberId;
        ingressMessageHeaderEncoder.leadershipTermId(leadershipTermId);
        sessionKeepAliveEncoder.leadershipTermId(leadershipTermId);

        if (ctx.clusterMemberEndpoints() != null)
        {
            CloseHelper.close(publication);
            ctx.clusterMemberEndpoints(memberEndpoints);
            updateMemberEndpoints(memberEndpoints, leaderMemberId);
        }

        fragmentAssembler.clear();
        controlledFragmentAssembler.clear();
        egressListener.newLeader(clusterSessionId, leadershipTermId, leaderMemberId, memberEndpoints);
        controlledEgressListener.newLeader(clusterSessionId, leadershipTermId, leaderMemberId, memberEndpoints);
    }

    static Int2ObjectHashMap<MemberEndpoint> parseMemberEndpoints(final String memberEndpoints)
    {
        final Int2ObjectHashMap<MemberEndpoint> endpointByMemberIdMap = new Int2ObjectHashMap<>();

        if (null != memberEndpoints)
        {
            for (final String endpoint : memberEndpoints.split(","))
            {
                final int i = endpoint.indexOf('=');
                if (-1 == i)
                {
                    throw new ConfigurationException("endpoint missing '=' separator: " + memberEndpoints);
                }

                final int memberId = AsciiEncoding.parseIntAscii(endpoint, 0, i);
                endpointByMemberIdMap.put(memberId, new MemberEndpoint(memberId, endpoint.substring(i + 1)));
            }
        }

        return endpointByMemberIdMap;
    }

    private void updateMemberEndpoints(final String memberEndpoints, final int leaderMemberId)
    {
        final Int2ObjectHashMap<MemberEndpoint> tempMap = parseMemberEndpoints(memberEndpoints);
        final MemberEndpoint existingLeaderEndpoint = endpointByMemberIdMap.get(leaderMemberId);
        final MemberEndpoint leaderEndpoint = tempMap.get(leaderMemberId);

        if (null != existingLeaderEndpoint && null != existingLeaderEndpoint.publication &&
            existingLeaderEndpoint.endpoint.equals(leaderEndpoint.endpoint))
        {
            leaderEndpoint.publication = existingLeaderEndpoint.publication;
            publication = existingLeaderEndpoint.publication;
            existingLeaderEndpoint.publication = null;
        }

        if (null == leaderEndpoint.publication)
        {
            final ChannelUri channelUri = ChannelUri.parse(ctx.ingressChannel());
            channelUri.put(CommonContext.ENDPOINT_PARAM_NAME, leaderEndpoint.endpoint);
            publication = addIngressPublication(ctx, channelUri.toString(), ctx.ingressStreamId());
            leaderEndpoint.publication = publication;
        }

        endpointByMemberIdMap.values().forEach(MemberEndpoint::disconnect);
        endpointByMemberIdMap = tempMap;
    }

    private void onFragment(final DirectBuffer buffer, final int offset, final int length, final Header header)
    {
        messageHeaderDecoder.wrap(buffer, offset);

        final int templateId = messageHeaderDecoder.templateId();
        if (EgressMessageHeaderDecoder.TEMPLATE_ID == templateId)
        {
            egressMessageHeaderDecoder.wrap(
                buffer,
                offset + MessageHeaderDecoder.ENCODED_LENGTH,
                messageHeaderDecoder.blockLength(),
                messageHeaderDecoder.version());

            final long sessionId = egressMessageHeaderDecoder.clusterSessionId();
            if (sessionId == clusterSessionId)
            {
                egressListener.onMessage(
                    sessionId,
                    egressMessageHeaderDecoder.timestamp(),
                    buffer,
                    offset + EGRESS_HEADER_LENGTH,
                    length - EGRESS_HEADER_LENGTH,
                    header);
            }
        }
        else if (NewLeaderEventDecoder.TEMPLATE_ID == templateId)
        {
            newLeaderEventDecoder.wrap(
                buffer,
                offset + MessageHeaderDecoder.ENCODED_LENGTH,
                messageHeaderDecoder.blockLength(),
                messageHeaderDecoder.version());

            final long sessionId = newLeaderEventDecoder.clusterSessionId();
            if (sessionId == clusterSessionId)
            {
                onNewLeader(
                    sessionId,
                    newLeaderEventDecoder.leadershipTermId(),
                    newLeaderEventDecoder.leaderMemberId(),
                    newLeaderEventDecoder.memberEndpoints());
            }
        }
        else if (SessionEventDecoder.TEMPLATE_ID == templateId)
        {
            sessionEventDecoder.wrap(
                buffer,
                offset + MessageHeaderDecoder.ENCODED_LENGTH,
                messageHeaderDecoder.blockLength(),
                messageHeaderDecoder.version());

            final long sessionId = sessionEventDecoder.clusterSessionId();
            if (sessionId == clusterSessionId)
            {
                egressListener.sessionEvent(
                    sessionEventDecoder.correlationId(),
                    sessionId,
                    sessionEventDecoder.leadershipTermId(),
                    sessionEventDecoder.leaderMemberId(),
                    sessionEventDecoder.code(),
                    sessionEventDecoder.detail());
            }
        }
    }

    private ControlledFragmentHandler.Action onControlledFragment(
        final DirectBuffer buffer, final int offset, final int length, final Header header)
    {
        messageHeaderDecoder.wrap(buffer, offset);

        final int templateId = messageHeaderDecoder.templateId();
        if (EgressMessageHeaderDecoder.TEMPLATE_ID == templateId)
        {
            egressMessageHeaderDecoder.wrap(
                buffer,
                offset + MessageHeaderDecoder.ENCODED_LENGTH,
                messageHeaderDecoder.blockLength(),
                messageHeaderDecoder.version());

            final long sessionId = egressMessageHeaderDecoder.clusterSessionId();
            if (sessionId == clusterSessionId)
            {
                return controlledEgressListener.onMessage(
                    sessionId,
                    egressMessageHeaderDecoder.timestamp(),
                    buffer,
                    offset + EGRESS_HEADER_LENGTH,
                    length - EGRESS_HEADER_LENGTH,
                    header);
            }
        }
        else if (NewLeaderEventDecoder.TEMPLATE_ID == templateId)
        {
            newLeaderEventDecoder.wrap(
                buffer,
                offset + MessageHeaderDecoder.ENCODED_LENGTH,
                messageHeaderDecoder.blockLength(),
                messageHeaderDecoder.version());

            final long sessionId = newLeaderEventDecoder.clusterSessionId();
            if (sessionId == clusterSessionId)
            {
                onNewLeader(
                    sessionId,
                    newLeaderEventDecoder.leadershipTermId(),
                    newLeaderEventDecoder.leaderMemberId(),
                    newLeaderEventDecoder.memberEndpoints());

                return ControlledFragmentHandler.Action.COMMIT;
            }
        }
        else if (SessionEventDecoder.TEMPLATE_ID == templateId)
        {
            sessionEventDecoder.wrap(
                buffer,
                offset + MessageHeaderDecoder.ENCODED_LENGTH,
                messageHeaderDecoder.blockLength(),
                messageHeaderDecoder.version());

            final long sessionId = sessionEventDecoder.clusterSessionId();
            if (sessionId == clusterSessionId)
            {
                controlledEgressListener.sessionEvent(
                    sessionEventDecoder.correlationId(),
                    sessionId,
                    sessionEventDecoder.leadershipTermId(),
                    sessionEventDecoder.leaderMemberId(),
                    sessionEventDecoder.code(),
                    sessionEventDecoder.detail());
            }
        }

        return ControlledFragmentHandler.Action.CONTINUE;
    }

    private void closeSession()
    {
        idleStrategy.reset();
        final int length = MessageHeaderEncoder.ENCODED_LENGTH + SessionCloseRequestEncoder.BLOCK_LENGTH;
        final SessionCloseRequestEncoder sessionCloseRequestEncoder = new SessionCloseRequestEncoder();
        int attempts = SEND_ATTEMPTS;

        while (true)
        {
            final long result = publication.tryClaim(length, bufferClaim);
            if (result > 0)
            {
                sessionCloseRequestEncoder
                    .wrapAndApplyHeader(bufferClaim.buffer(), bufferClaim.offset(), messageHeaderEncoder)
                    .clusterSessionId(clusterSessionId);

                bufferClaim.commit();
                break;
            }

            checkResult(result);

            if (--attempts <= 0)
            {
                break;
            }

            idleStrategy.idle();
        }
    }

    private static Publication addIngressPublication(final Context ctx, final String channel, final int streamId)
    {
        if (ctx.isIngressExclusive())
        {
            return ctx.aeron().addExclusivePublication(channel, streamId);
        }
        else
        {
            return ctx.aeron().addPublication(channel, streamId);
        }
    }

    private static void checkResult(final long result)
    {
        if (result == Publication.NOT_CONNECTED ||
            result == Publication.CLOSED ||
            result == Publication.MAX_POSITION_EXCEEDED)
        {
            throw new ClusterException("unexpected publication state: " + result);
        }
    }

    /**
     * Configuration options for cluster client.
     */
    public static class Configuration
    {
        public static final int MAJOR_VERSION = 0;
        public static final int MINOR_VERSION = 0;
        public static final int PATCH_VERSION = 1;
        public static final int SEMANTIC_VERSION = SemanticVersion.compose(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION);

        /**
         * Timeout when waiting on a message to be sent or received.
         */
        public static final String MESSAGE_TIMEOUT_PROP_NAME = "aeron.cluster.message.timeout";

        /**
         * Default timeout when waiting on a message to be sent or received.
         */
        public static final long MESSAGE_TIMEOUT_DEFAULT_NS = TimeUnit.SECONDS.toNanos(5);

        /**
         * Property name for the comma separated list of cluster member endpoints for use with unicast. This is the
         * endpoint values which get substituted into the {@link #INGRESS_CHANNEL_PROP_NAME} when using UDP unicast.
         *

         * {@code "0=endpoint,1=endpoint,2=endpoint"}
         *

         * Each member of the list will be substituted for the endpoint in the {@link #INGRESS_CHANNEL_PROP_NAME} value.
         */
        public static final String CLUSTER_MEMBER_ENDPOINTS_PROP_NAME = "aeron.cluster.member.endpoints";

        /**
         * Default comma separated list of cluster member endpoints.
         */
        public static final String CLUSTER_MEMBER_ENDPOINTS_DEFAULT = null;

        /**
         * Channel for sending messages to a cluster. Ideally this will be a multicast address otherwise unicast will
         * be required and the {@link #CLUSTER_MEMBER_ENDPOINTS_PROP_NAME} is used to substitute the endpoints from
         * the {@link #CLUSTER_MEMBER_ENDPOINTS_PROP_NAME} list.
         */
        public static final String INGRESS_CHANNEL_PROP_NAME = "aeron.cluster.ingress.channel";

        /**
         * Channel for sending messages to a cluster.
         */
        public static final String INGRESS_CHANNEL_DEFAULT = "aeron:udp?endpoint=localhost:9010";

        /**
         * Stream id within a channel for sending messages to a cluster.
         */
        public static final String INGRESS_STREAM_ID_PROP_NAME = "aeron.cluster.ingress.stream.id";

        /**
         * Default stream id within a channel for sending messages to a cluster.
         */
        public static final int INGRESS_STREAM_ID_DEFAULT = 101;

        /**
         * Channel for receiving response messages from a cluster.
         */
        public static final String EGRESS_CHANNEL_PROP_NAME = "aeron.cluster.egress.channel";

        /**
         * Channel for receiving response messages from a cluster.
         */
        public static final String EGRESS_CHANNEL_DEFAULT = "aeron:udp?endpoint=localhost:9020";

        /**
         * Stream id within a channel for receiving messages from a cluster.
         */
        public static final String EGRESS_STREAM_ID_PROP_NAME = "aeron.cluster.egress.stream.id";

        /**
         * Default stream id within a channel for receiving messages from a cluster.
         */
        public static final int EGRESS_STREAM_ID_DEFAULT = 102;

        /**
         * The timeout in nanoseconds to wait for a message.
         *
         * @return timeout in nanoseconds to wait for a message.
         * @see #MESSAGE_TIMEOUT_PROP_NAME
         */
        public static long messageTimeoutNs()
        {
            return getDurationInNanos(MESSAGE_TIMEOUT_PROP_NAME, MESSAGE_TIMEOUT_DEFAULT_NS);
        }

        /**
         * The value {@link #CLUSTER_MEMBER_ENDPOINTS_DEFAULT} or system property
         * {@link #CLUSTER_MEMBER_ENDPOINTS_PROP_NAME} if set.
         *
         * @return {@link #CLUSTER_MEMBER_ENDPOINTS_DEFAULT} or system property
         * {@link #CLUSTER_MEMBER_ENDPOINTS_PROP_NAME} if set.
         */
        public static String clusterMemberEndpoints()
        {
            return System.getProperty(CLUSTER_MEMBER_ENDPOINTS_PROP_NAME, CLUSTER_MEMBER_ENDPOINTS_DEFAULT);
        }

        /**
         * The value {@link #INGRESS_CHANNEL_DEFAULT} or system property
         * {@link #INGRESS_CHANNEL_PROP_NAME} if set.
         *
         * @return {@link #INGRESS_CHANNEL_DEFAULT} or system property
         * {@link #INGRESS_CHANNEL_PROP_NAME} if set.
         */
        public static String ingressChannel()
        {
            return System.getProperty(INGRESS_CHANNEL_PROP_NAME, INGRESS_CHANNEL_DEFAULT);
        }

        /**
         * The value {@link #INGRESS_STREAM_ID_DEFAULT} or system property
         * {@link #INGRESS_STREAM_ID_PROP_NAME} if set.
         *
         * @return {@link #INGRESS_STREAM_ID_DEFAULT} or system property
         * {@link #INGRESS_STREAM_ID_PROP_NAME} if set.
         */
        public static int ingressStreamId()
        {
            return Integer.getInteger(INGRESS_STREAM_ID_PROP_NAME, INGRESS_STREAM_ID_DEFAULT);
        }

        /**
         * The value {@link #EGRESS_CHANNEL_DEFAULT} or system property
         * {@link #EGRESS_CHANNEL_PROP_NAME} if set.
         *
         * @return {@link #EGRESS_CHANNEL_DEFAULT} or system property
         * {@link #EGRESS_CHANNEL_PROP_NAME} if set.
         */
        public static String egressChannel()
        {
            return System.getProperty(EGRESS_CHANNEL_PROP_NAME, EGRESS_CHANNEL_DEFAULT);
        }

        /**
         * The value {@link #EGRESS_STREAM_ID_DEFAULT} or system property
         * {@link #EGRESS_STREAM_ID_PROP_NAME} if set.
         *
         * @return {@link #EGRESS_STREAM_ID_DEFAULT} or system property
         * {@link #EGRESS_STREAM_ID_PROP_NAME} if set.
         */
        public static int egressStreamId()
        {
            return Integer.getInteger(EGRESS_STREAM_ID_PROP_NAME, EGRESS_STREAM_ID_DEFAULT);
        }
    }

    /**
     * Context for cluster session and connection.
     */
    public static class Context implements Cloneable
    {
        private long messageTimeoutNs = Configuration.messageTimeoutNs();
        private String clusterMemberEndpoints = Configuration.clusterMemberEndpoints();
        private String ingressChannel = Configuration.ingressChannel();
        private int ingressStreamId = Configuration.ingressStreamId();
        private String egressChannel = Configuration.egressChannel();
        private int egressStreamId = Configuration.egressStreamId();
        private IdleStrategy idleStrategy;
        private String aeronDirectoryName = CommonContext.getAeronDirectoryName();
        private Aeron aeron;
        private CredentialsSupplier credentialsSupplier;
        private boolean ownsAeronClient = false;
        private boolean isIngressExclusive = false;
        private ErrorHandler errorHandler = Aeron.Configuration.DEFAULT_ERROR_HANDLER;
        private boolean isDirectAssemblers = false;
        private EgressListener egressListener;
        private ControlledEgressListener controlledEgressListener;

        /**
         * Perform a shallow copy of the object.
         *
         * @return a shallow copy of the object.
         */
        public Context clone()
        {
            try
            {
                return (Context)super.clone();
            }
            catch (final CloneNotSupportedException ex)
            {
                throw new RuntimeException(ex);
            }
        }

        public void conclude()
        {
            if (null == aeron)
            {
                aeron = Aeron.connect(
                    new Aeron.Context()
                        .aeronDirectoryName(aeronDirectoryName)
                        .errorHandler(errorHandler));

                ownsAeronClient = true;
            }

            if (null == idleStrategy)
            {
                idleStrategy = new BackoffIdleStrategy(1, 10, 1000, 1000);
            }

            if (null == credentialsSupplier)
            {
                credentialsSupplier = new NullCredentialsSupplier();
            }

            if (null == egressListener)
            {
                egressListener =
                    (clusterSessionId, timestamp, buffer, offset, length, header) ->
                    {
                        throw new ConfigurationException(
                            "egressListener must be specified on AeronCluster.Context");
                    };
            }

            if (null == controlledEgressListener)
            {
                controlledEgressListener =
                    (clusterSessionId, timestamp, buffer, offset, length, header) ->
                    {
                        throw new ConfigurationException(
                            "controlledEgressListener must be specified on AeronCluster.Context");
                    };
            }
        }

        /**
         * Set the message timeout in nanoseconds to wait for sending or receiving a message.
         *
         * @param messageTimeoutNs to wait for sending or receiving a message.
         * @return this for a fluent API.
         * @see Configuration#MESSAGE_TIMEOUT_PROP_NAME
         */
        public Context messageTimeoutNs(final long messageTimeoutNs)
        {
            this.messageTimeoutNs = messageTimeoutNs;
            return this;
        }

        /**
         * The message timeout in nanoseconds to wait for sending or receiving a message.
         *
         * @return the message timeout in nanoseconds to wait for sending or receiving a message.
         * @see Configuration#MESSAGE_TIMEOUT_PROP_NAME
         */
        public long messageTimeoutNs()
        {
            return messageTimeoutNs;
        }

        /**
         * The endpoints representing members for use with unicast to be substituted into the {@link #ingressChannel()}
         * for endpoints. A null value can be used when multicast where the {@link #ingressChannel()} contains the
         * multicast endpoint.
         *
         * @param clusterMembers which are all candidates to be leader.
         * @return this for a fluent API.
         * @see Configuration#CLUSTER_MEMBER_ENDPOINTS_PROP_NAME
         */
        public Context clusterMemberEndpoints(final String clusterMembers)
        {
            this.clusterMemberEndpoints = clusterMembers;
            return this;
        }

        /**
         * The endpoints representing members for use with unicast to be substituted into the {@link #ingressChannel()}
         * for endpoints. A null value can be used when multicast where the {@link #ingressChannel()} contains the
         * multicast endpoint.
         *
         * @return members of the cluster which are all candidates to be leader.
         * @see Configuration#CLUSTER_MEMBER_ENDPOINTS_PROP_NAME
         */
        public String clusterMemberEndpoints()
        {
            return clusterMemberEndpoints;
        }

        /**
         * Set the channel parameter for the ingress channel.
         *

         * The endpoints representing members for use with unicast are substituted from the
         * {@link #clusterMemberEndpoints()} for endpoints. A null value can be used when multicast
         * where this contains the multicast endpoint.
         *
         * @param channel parameter for the ingress channel.
         * @return this for a fluent API.
         * @see Configuration#INGRESS_CHANNEL_PROP_NAME
         */
        public Context ingressChannel(final String channel)
        {
            ingressChannel = channel;
            return this;
        }

        /**
         * Get the channel parameter for the ingress channel.
         *

         * The endpoints representing members for use with unicast are substituted from the
         * {@link #clusterMemberEndpoints()} for endpoints. A null value can be used when multicast
         * where this contains the multicast endpoint.
         *
         * @return the channel parameter for the ingress channel.
         * @see Configuration#INGRESS_CHANNEL_PROP_NAME
         */
        public String ingressChannel()
        {
            return ingressChannel;
        }

        /**
         * Set the stream id for the ingress channel.
         *
         * @param streamId for the ingress channel.
         * @return this for a fluent API
         * @see Configuration#INGRESS_STREAM_ID_PROP_NAME
         */
        public Context ingressStreamId(final int streamId)
        {
            ingressStreamId = streamId;
            return this;
        }

        /**
         * Get the stream id for the ingress channel.
         *
         * @return the stream id for the ingress channel.
         * @see Configuration#INGRESS_STREAM_ID_PROP_NAME
         */
        public int ingressStreamId()
        {
            return ingressStreamId;
        }

        /**
         * Set the channel parameter for the egress channel.
         *
         * @param channel parameter for the egress channel.
         * @return this for a fluent API.
         * @see Configuration#EGRESS_CHANNEL_PROP_NAME
         */
        public Context egressChannel(final String channel)
        {
            egressChannel = channel;
            return this;
        }

        /**
         * Get the channel parameter for the egress channel.
         *
         * @return the channel parameter for the egress channel.
         * @see Configuration#EGRESS_CHANNEL_PROP_NAME
         */
        public String egressChannel()
        {
            return egressChannel;
        }

        /**
         * Set the stream id for the egress channel.
         *
         * @param streamId for the egress channel.
         * @return this for a fluent API
         * @see Configuration#EGRESS_STREAM_ID_PROP_NAME
         */
        public Context egressStreamId(final int streamId)
        {
            egressStreamId = streamId;
            return this;
        }

        /**
         * Get the stream id for the egress channel.
         *
         * @return the stream id for the egress channel.
         * @see Configuration#EGRESS_STREAM_ID_PROP_NAME
         */
        public int egressStreamId()
        {
            return egressStreamId;
        }

        /**
         * Set the {@link IdleStrategy} used when waiting for responses.
         *
         * @param idleStrategy used when waiting for responses.
         * @return this for a fluent API.
         */
        public Context idleStrategy(final IdleStrategy idleStrategy)
        {
            this.idleStrategy = idleStrategy;
            return this;
        }

        /**
         * Get the {@link IdleStrategy} used when waiting for responses.
         *
         * @return the {@link IdleStrategy} used when waiting for responses.
         */
        public IdleStrategy idleStrategy()
        {
            return idleStrategy;
        }

        /**
         * Set the top level Aeron directory used for communication between the Aeron client and Media Driver.
         *
         * @param aeronDirectoryName the top level Aeron directory.
         * @return this for a fluent API.
         */
        public Context aeronDirectoryName(final String aeronDirectoryName)
        {
            this.aeronDirectoryName = aeronDirectoryName;
            return this;
        }

        /**
         * Get the top level Aeron directory used for communication between the Aeron client and Media Driver.
         *
         * @return The top level Aeron directory.
         */
        public String aeronDirectoryName()
        {
            return aeronDirectoryName;
        }

        /**
         * {@link Aeron} client for communicating with the local Media Driver.
         *

         * This client will be closed when the {@link AeronCluster#close()} or {@link #close()} methods are called if
         * {@link #ownsAeronClient()} is true.
         *
         * @param aeron client for communicating with the local Media Driver.
         * @return this for a fluent API.
         * @see Aeron#connect()
         */
        public Context aeron(final Aeron aeron)
        {
            this.aeron = aeron;
            return this;
        }

        /**
         * {@link Aeron} client for communicating with the local Media Driver.
         *

         * If not provided then a default will be established during {@link #conclude()} by calling
         * {@link Aeron#connect()}.
         *
         * @return client for communicating with the local Media Driver.
         */
        public Aeron aeron()
        {
            return aeron;
        }

        /**
         * Does this context own the {@link #aeron()} client and this takes responsibility for closing it?
         *
         * @param ownsAeronClient does this context own the {@link #aeron()} client.
         * @return this for a fluent API.
         */
        public Context ownsAeronClient(final boolean ownsAeronClient)
        {
            this.ownsAeronClient = ownsAeronClient;
            return this;
        }

        /**
         * Does this context own the {@link #aeron()} client and this takes responsibility for closing it?
         *
         * @return does this context own the {@link #aeron()} client and this takes responsibility for closing it?
         */
        public boolean ownsAeronClient()
        {
            return ownsAeronClient;
        }

        /**
         * Is ingress to the cluster exclusively from a single thread for this client?
         *
         * @param isIngressExclusive true if ingress to the cluster is exclusively from a single thread for this client?
         * @return this for a fluent API.
         */
        public Context isIngressExclusive(final boolean isIngressExclusive)
        {
            this.isIngressExclusive = isIngressExclusive;
            return this;
        }

        /**
         * Is ingress to the cluster exclusively from a single thread for this client?
         *
         * @return true if ingress to the cluster exclusively from a single thread for this client?
         */
        public boolean isIngressExclusive()
        {
            return isIngressExclusive;
        }

        /**
         * Set the {@link CredentialsSupplier} to be used for authentication with the cluster.
         *
         * @param credentialsSupplier to be used for authentication with the cluster.
         * @return this for fluent API.
         */
        public Context credentialsSupplier(final CredentialsSupplier credentialsSupplier)
        {
            this.credentialsSupplier = credentialsSupplier;
            return this;
        }

        /**
         * Get the {@link CredentialsSupplier} to be used for authentication with the cluster.
         *
         * @return the {@link CredentialsSupplier} to be used for authentication with the cluster.
         */
        public CredentialsSupplier credentialsSupplier()
        {
            return credentialsSupplier;
        }

        /**
         * Set the {@link ErrorHandler} to be used for handling any exceptions.
         *
         * @param errorHandler Method to handle objects of type Throwable.
         * @return this for fluent API.
         */
        public Context errorHandler(final ErrorHandler errorHandler)
        {
            this.errorHandler = errorHandler;
            return this;
        }

        /**
         * Get the {@link ErrorHandler} to be used for handling any exceptions.
         *
         * @return The {@link ErrorHandler} to be used for handling any exceptions.
         */
        public ErrorHandler errorHandler()
        {
            return errorHandler;
        }

        /**
         * Are direct buffers used for fragment assembly on egress?
         *
         * @param isDirectAssemblers true if direct buffers used for fragment assembly on egress.
         * @return this for a fluent API.
         */
        public Context isDirectAssemblers(final boolean isDirectAssemblers)
        {
            this.isDirectAssemblers = isDirectAssemblers;
            return this;
        }

        /**
         * Are direct buffers used for fragment assembly on egress?
         *
         * @return true if direct buffers used for fragment assembly on egress.
         */
        public boolean isDirectAssemblers()
        {
            return isDirectAssemblers;
        }

        /**
         * Set the {@link EgressListener} function that will be called when polling for egress via
         * {@link AeronCluster#pollEgress()}.
         *

         * Only {@link EgressListener#onMessage(long, long, DirectBuffer, int, int, Header)} will be dispatched
         * when using {@link AeronCluster#pollEgress()}.
         *
         * @param listener function that will be called when polling for egress via {@link AeronCluster#pollEgress()}.
         * @return this for a fluent API.
         */
        public Context egressListener(final EgressListener listener)
        {
            this.egressListener = listener;
            return this;
        }

        /**
         * Get the {@link EgressListener} function that will be called when polling for egress via
         * {@link AeronCluster#pollEgress()}.
         *
         * @return the {@link EgressListener} function that will be called when polling for egress via
         * {@link AeronCluster#pollEgress()}.
         */
        public EgressListener egressListener()
        {
            return egressListener;
        }

        /**
         * Set the {@link ControlledEgressListener} function that will be called when polling for egress via
         * {@link AeronCluster#controlledPollEgress()}.
         *

         * Only {@link ControlledEgressListener#onMessage(long, long, DirectBuffer, int, int, Header)} will be
         * dispatched when using {@link AeronCluster#controlledPollEgress()}.
         *
         * @param listener function that will be called when polling for egress via
         *                 {@link AeronCluster#controlledPollEgress()}.
         * @return this for a fluent API.
         */
        public Context controlledEgressListener(final ControlledEgressListener listener)
        {
            this.controlledEgressListener = listener;
            return this;
        }

        /**
         * Get the {@link ControlledEgressListener} function that will be called when polling for egress via
         * {@link AeronCluster#controlledPollEgress()}.
         *
         * @return the {@link ControlledEgressListener} function that will be called when polling for egress via
         * {@link AeronCluster#controlledPollEgress()}.
         */
        public ControlledEgressListener controlledEgressListener()
        {
            return controlledEgressListener;
        }

        /**
         * Close the context and free applicable resources.
         *

         * If {@link #ownsAeronClient()} is true then the {@link #aeron()} client will be closed.
         */
        public void close()
        {
            if (ownsAeronClient)
            {
                CloseHelper.close(aeron);
            }
        }
    }

    /**
     * Allows for the async establishment of a cluster session. {@link #poll()} should be called repeatedly until
     * it returns a non-null value with the new {@link AeronCluster} client. On error {@link #close()} should be called
     * to clean up allocated resources.
     */
    public static class AsyncConnect implements AutoCloseable
    {
        private final long deadlineNs;
        private long correlationId;
        private long clusterSessionId;
        private long leadershipTermId;
        private int leaderMemberId;
        private int step = 0;

        private final Context ctx;
        private final NanoClock nanoClock;
        private final EgressPoller egressPoller;
        private final ExpandableArrayBuffer buffer = new ExpandableArrayBuffer();
        private final MessageHeaderEncoder messageHeaderEncoder = new MessageHeaderEncoder();
        private Int2ObjectHashMap<MemberEndpoint> endpointByMemberIdMap;
        private Publication ingressPublication;

        AsyncConnect(final Context ctx, final Subscription egressSubscription, final long deadlineNs)
        {
            this.ctx = ctx;

            endpointByMemberIdMap = parseMemberEndpoints(ctx.clusterMemberEndpoints());
            egressPoller = new EgressPoller(egressSubscription, FRAGMENT_LIMIT);
            nanoClock = ctx.aeron().context().nanoClock();
            this.deadlineNs = deadlineNs;
        }

        /**
         * Close allocated resources. Must be called on error. On success this is a no op.
         */
        public void close()
        {
            if (5 != step)
            {
                CloseHelper.close(ingressPublication);
                endpointByMemberIdMap.values().forEach(MemberEndpoint::disconnect);
                ctx.close();
            }
        }

        /**
         * Indicates which step in the connect process has been reached.
         *
         * @return which step in the connect process has been reached.
         */
        public int step()
        {
            return step;
        }

        private void step(final int step)
        {
            //System.out.println(this.step + " -> " + step);
            this.step = step;
        }

        /**
         * Poll to advance steps in the connection until complete or error.
         *
         * @return null if not yet complete then {@link AeronCluster} when complete.
         */
        public AeronCluster poll()
        {
            AeronCluster aeronCluster = null;
            checkDeadline();

            switch (step)
            {
                case 0:
                    createIngressPublications();
                    break;

                case 1:
                    awaitPublicationConnected();
                    break;

                case 2:
                    sendMessage();
                    break;

                case 3:
                    pollResponse();
                    break;
            }

            if (4 == step)
            {
                aeronCluster = newInstance();
                ingressPublication = null;
                final MemberEndpoint endpoint = endpointByMemberIdMap.get(leaderMemberId);
                if (null != endpoint)
                {
                    endpoint.publication = null;
                }
                endpointByMemberIdMap.values().forEach(MemberEndpoint::disconnect);

                step(5);
            }

            return aeronCluster;
        }

        private void checkDeadline()
        {
            if (Thread.interrupted())
            {
                LangUtil.rethrowUnchecked(new InterruptedException());
            }

            if (deadlineNs - nanoClock.nanoTime() < 0)
            {
                throw new TimeoutException("connect timeout, step=" + step);
            }
        }

        private void createIngressPublications()
        {
            if (ctx.clusterMemberEndpoints() == null)
            {
                ingressPublication = addIngressPublication(ctx, ctx.ingressChannel(), ctx.ingressStreamId());
            }
            else
            {
                final ChannelUri channelUri = ChannelUri.parse(ctx.ingressChannel());
                for (final MemberEndpoint member : endpointByMemberIdMap.values())
                {
                    channelUri.put(CommonContext.ENDPOINT_PARAM_NAME, member.endpoint);
                    member.publication = addIngressPublication(ctx, channelUri.toString(), ctx.ingressStreamId());
                }
            }

            step(1);
        }

        private void awaitPublicationConnected()
        {
            if (null != ingressPublication && ingressPublication.isConnected())
            {
                prepareConnectRequest();
            }
            else
            {
                for (final MemberEndpoint member : endpointByMemberIdMap.values())
                {
                    if (null != member.publication && member.publication.isConnected())
                    {
                        ingressPublication = member.publication;
                        prepareConnectRequest();
                        break;
                    }
                }
            }
        }

        private void prepareConnectRequest()
        {
            correlationId = ctx.aeron().nextCorrelationId();
            final byte[] encodedCredentials = ctx.credentialsSupplier().encodedCredentials();

            new SessionConnectRequestEncoder()
                .wrapAndApplyHeader(buffer, 0, messageHeaderEncoder)
                .correlationId(correlationId)
                .responseStreamId(ctx.egressStreamId())
                .version(Configuration.SEMANTIC_VERSION)
                .responseChannel(ctx.egressChannel())
                .putEncodedCredentials(encodedCredentials, 0, encodedCredentials.length);

            step(2);
        }

        private void sendMessage()
        {
            final long result = ingressPublication.offer(buffer);
            if (result > 0)
            {
                step(3);
            }
            else if (Publication.CLOSED == result)
            {
                throw new ClusterException("unexpected close from cluster");
            }
        }

        private void pollResponse()
        {
            if (egressPoller.poll() > 0 &&
                egressPoller.isPollComplete() &&
                egressPoller.correlationId() == correlationId)
            {
                if (egressPoller.isChallenged())
                {
                    clusterSessionId = egressPoller.clusterSessionId();
                    prepareChallengeResponse(ctx.credentialsSupplier().onChallenge(egressPoller.encodedChallenge()));
                    step(2);
                    return;
                }

                switch (egressPoller.eventCode())
                {
                    case OK:
                        leadershipTermId = egressPoller.leadershipTermId();
                        leaderMemberId = egressPoller.leaderMemberId();
                        clusterSessionId = egressPoller.clusterSessionId();
                        step(4);
                        break;

                    case ERROR:
                        throw new ClusterException(egressPoller.detail());

                    case REDIRECT:
                        updateMembers();
                        break;

                    case AUTHENTICATION_REJECTED:
                        throw new AuthenticationException(egressPoller.detail());
                }
            }
        }

        private void prepareChallengeResponse(final byte[] encodedCredentials)
        {
            correlationId = ctx.aeron().nextCorrelationId();

            new ChallengeResponseEncoder()
                .wrapAndApplyHeader(buffer, 0, messageHeaderEncoder)
                .correlationId(correlationId)
                .clusterSessionId(clusterSessionId)
                .putEncodedCredentials(encodedCredentials, 0, encodedCredentials.length);

            step(2);
        }

        private void updateMembers()
        {
            leaderMemberId = egressPoller.leaderMemberId();
            final MemberEndpoint leaderEndpoint = endpointByMemberIdMap.get(leaderMemberId);
            if (null != leaderEndpoint)
            {
                ingressPublication = leaderEndpoint.publication;
                leaderEndpoint.publication = null;
                endpointByMemberIdMap.values().forEach(MemberEndpoint::disconnect);
                endpointByMemberIdMap = parseMemberEndpoints(egressPoller.detail());
            }
            else
            {
                endpointByMemberIdMap.values().forEach(MemberEndpoint::disconnect);
                endpointByMemberIdMap = parseMemberEndpoints(egressPoller.detail());

                final MemberEndpoint memberEndpoint = endpointByMemberIdMap.get(leaderMemberId);
                final ChannelUri channelUri = ChannelUri.parse(ctx.ingressChannel());
                channelUri.put(CommonContext.ENDPOINT_PARAM_NAME, memberEndpoint.endpoint);
                memberEndpoint.publication = addIngressPublication(ctx, channelUri.toString(), ctx.ingressStreamId());
                ingressPublication = memberEndpoint.publication;
            }

            step(1);
        }

        private AeronCluster newInstance()
        {
            return new AeronCluster(
                ctx,
                messageHeaderEncoder,
                ingressPublication,
                egressPoller.subscription(),
                endpointByMemberIdMap,
                clusterSessionId,
                leadershipTermId,
                leaderMemberId);
        }
    }

    static final class MemberEndpoint
    {
        final int memberId;
        final String endpoint;
        Publication publication;

        MemberEndpoint(final int memberId, final String endpoint)
        {
            this.memberId = memberId;
            this.endpoint = endpoint;
        }

        void disconnect()
        {
            CloseHelper.close(publication);
            publication = null;
        }

        public String toString()
        {
            return "MemberEndpoint{" +
                "memberId=" + memberId +
                ", endpoint='" + endpoint + '\'' +
                ", publication=" + publication +
                '}';
        }
    }
}


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

/**
 * Provides the functionality to unblock a log at a given position.
 */
public class LogBufferUnblocker
{
    /**
     * Attempt to unblock a log buffer at given position
     *
     * @param termBuffers       for current blockedOffset
     * @param logMetaDataBuffer for log buffer
     * @param blockedPosition   to attempt to unblock
     * @param termLength        of the buffer for each term in the log
     * @return whether unblocked or not
     */
    public static boolean unblock(
        final UnsafeBuffer[] termBuffers,
        final UnsafeBuffer logMetaDataBuffer,
        final long blockedPosition,
        final int termLength)
    {
        final int positionBitsToShift = LogBufferDescriptor.positionBitsToShift(termLength);
        final int blockedTermCount = (int)(blockedPosition >> positionBitsToShift);
        final int blockedOffset = (int)blockedPosition & (termLength - 1);
        final int activeTermCount = activeTermCount(logMetaDataBuffer);

        if (activeTermCount == (blockedTermCount - 1) && blockedOffset == 0)
        {
            final int currentTermId = termId(rawTailVolatile(logMetaDataBuffer, indexByTermCount(activeTermCount)));
            return rotateLog(logMetaDataBuffer, activeTermCount, currentTermId);
        }

        final int blockedIndex = indexByTermCount(blockedTermCount);
        final long rawTail = rawTailVolatile(logMetaDataBuffer, blockedIndex);
        final int termId = termId(rawTail);
        final int tailOffset = termOffset(rawTail, termLength);
        final UnsafeBuffer termBuffer = termBuffers[blockedIndex];

        switch (TermUnblocker.unblock(logMetaDataBuffer, termBuffer, blockedOffset, tailOffset, termId))
        {
            case UNBLOCKED_TO_END:
                rotateLog(logMetaDataBuffer, blockedTermCount, termId);
                // fall through
            case UNBLOCKED:
                return true;
        }

        return false;
    }
}


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

import org.agrona.MutableDirectBuffer;

import static org.agrona.BitUtil.SIZE_OF_INT;
import static org.agrona.BitUtil.SIZE_OF_LONG;

/**
 * Control message flyweight for any message that needs to represent a connection
 *

 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                        Correlation ID                         |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                 Subscription Registration ID                  |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                          Stream ID                            |
 *  +---------------------------------------------------------------+
 *  |                       Channel Length                          |
 *  +---------------------------------------------------------------+
 *  |                       Channel (ASCII)                        ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *

 */
public class ImageMessageFlyweight
{
    private static final int CORRELATION_ID_OFFSET = 0;
    private static final int SUBSCRIPTION_REGISTRATION_ID_OFFSET = CORRELATION_ID_OFFSET + SIZE_OF_LONG;
    private static final int STREAM_ID_FIELD_OFFSET = SUBSCRIPTION_REGISTRATION_ID_OFFSET + SIZE_OF_LONG;
    private static final int CHANNEL_OFFSET = STREAM_ID_FIELD_OFFSET + SIZE_OF_INT;

    private MutableDirectBuffer buffer;
    private int offset;
    private int lengthOfChannel;

    /**
     * Wrap the buffer at a given offset for updates.
     *
     * @param buffer to wrap
     * @param offset at which the message begins.
     * @return for fluent API
     */
    public final ImageMessageFlyweight wrap(final MutableDirectBuffer buffer, final int offset)
    {
        this.buffer = buffer;
        this.offset = offset;

        return this;
    }

    /**
     * return correlation id field
     *
     * @return correlation id field
     */
    public long correlationId()
    {
        return buffer.getLong(offset + CORRELATION_ID_OFFSET);
    }

    /**
     * set correlation id field
     *
     * @param correlationId field value
     * @return flyweight
     */
    public ImageMessageFlyweight correlationId(final long correlationId)
    {
        buffer.putLong(offset + CORRELATION_ID_OFFSET, correlationId);

        return this;
    }

    /**
     * Registration ID for the subscription.
     *
     * @return registration ID for the subscription.
     */
    public long subscriptionRegistrationId()
    {
        return buffer.getLong(offset + SUBSCRIPTION_REGISTRATION_ID_OFFSET);
    }

    /**
     * Set the registration ID for the subscription.
     *
     * @param registrationId for the subscription
     * @return flyweight
     */
    public ImageMessageFlyweight subscriptionRegistrationId(final long registrationId)
    {
        buffer.putLong(offset + SUBSCRIPTION_REGISTRATION_ID_OFFSET, registrationId);

        return this;
    }

    /**
     * return stream id field
     *
     * @return stream id field
     */
    public int streamId()
    {
        return buffer.getInt(offset + STREAM_ID_FIELD_OFFSET);
    }

    /**
     * set stream id field
     *
     * @param streamId field value
     * @return flyweight
     */
    public ImageMessageFlyweight streamId(final int streamId)
    {
        buffer.putInt(offset + STREAM_ID_FIELD_OFFSET, streamId);

        return this;
    }

    /**
     * Get the channel field as ASCII
     *
     * @return channel field
     */
    public String channel()
    {
        final int length = buffer.getInt(offset + CHANNEL_OFFSET);
        lengthOfChannel = SIZE_OF_INT + length;

        return buffer.getStringAscii(offset + CHANNEL_OFFSET, length);
    }

    /**
     * Set the channel field as ASCII
     *
     * @param channel field value
     * @return flyweight
     */
    public ImageMessageFlyweight channel(final String channel)
    {
        lengthOfChannel = buffer.putStringAscii(offset + CHANNEL_OFFSET, channel);

        return this;
    }

    /**
     * Get the length of the current message
     *

     * NB: must be called after the data is written in order to be accurate.
     *
     * @return the length of the current message
     */
    public int length()
    {
        return CHANNEL_OFFSET + lengthOfChannel;
    }
}


/*
 * 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.BitUtil;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;

import static io.aeron.protocol.DataHeaderFlyweight.HEADER_LENGTH;
import static org.agrona.BitUtil.*;

/**
 * Layout description for log buffers which contains partitions of terms with associated term meta data,
 * plus ending with overall log meta data.
 *

 *  +----------------------------+
 *  |           Term 0           |
 *  +----------------------------+
 *  |           Term 1           |
 *  +----------------------------+
 *  |           Term 2           |
 *  +----------------------------+
 *  |        Log Meta Data       |
 *  +----------------------------+
 *

 */
public class LogBufferDescriptor
{
    /**
     * The number of partitions the log is divided into.
     */
    public static final int PARTITION_COUNT = 3;

    /**
     * Section index for which buffer contains the log meta data.
     */
    public static final int LOG_META_DATA_SECTION_INDEX = PARTITION_COUNT;

    /**
     * Minimum buffer length for a log term
     */
    public static final int TERM_MIN_LENGTH = 64 * 1024;

    /**
     * Maximum buffer length for a log term
     */
    public static final int TERM_MAX_LENGTH = 1024 * 1024 * 1024;

    /**
     * Minimum page size
     */
    public static final int PAGE_MIN_SIZE = 4 * 1024;

    /**
     * Maximum page size
     */
    public static final int PAGE_MAX_SIZE = 1024 * 1024 * 1024;

    // *******************************
    // *** Log Meta Data Constants ***
    // *******************************

    /**
     * Offset within the metadata where the tail values are stored.
     */
    public static final int TERM_TAIL_COUNTERS_OFFSET;

    /**
     * Offset within the log metadata where the active partition index is stored.
     */
    public static final int LOG_ACTIVE_TERM_COUNT_OFFSET;

    /**
     * Offset within the log metadata where the position of the End of Stream is stored.
     */
    public static final int LOG_END_OF_STREAM_POSITION_OFFSET;

    /**
     * Offset within the log metadata where whether the log is connected or not is stored.
     */
    public static final int LOG_IS_CONNECTED_OFFSET;

    /**
     * Offset within the log metadata where the active term id is stored.
     */
    public static final int LOG_INITIAL_TERM_ID_OFFSET;

    /**
     * Offset within the log metadata which the length field for the frame header is stored.
     */
    public static final int LOG_DEFAULT_FRAME_HEADER_LENGTH_OFFSET;

    /**
     * Offset within the log metadata which the MTU length is stored.
     */
    public static final int LOG_MTU_LENGTH_OFFSET;

    /**
     * Offset within the log metadata which the correlation id is stored.
     */
    public static final int LOG_CORRELATION_ID_OFFSET;

    /**
     * Offset within the log metadata which the term length is stored.
     */
    public static final int LOG_TERM_LENGTH_OFFSET;

    /**
     * Offset within the log metadata which the page size is stored.
     */
    public static final int LOG_PAGE_SIZE_OFFSET;

    /**
     * Offset at which the default frame headers begin.
     */
    public static final int LOG_DEFAULT_FRAME_HEADER_OFFSET;

    /**
     * Maximum length of a frame header.
     */
    public static final int LOG_DEFAULT_FRAME_HEADER_MAX_LENGTH = CACHE_LINE_LENGTH * 2;

    /**
     * Total length of the log metadata buffer in bytes.
     *

     *   0                   1                   2                   3
     *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *  |                       Tail Counter 0                          |
     *  |                                                               |
     *  +---------------------------------------------------------------+
     *  |                       Tail Counter 1                          |
     *  |                                                               |
     *  +---------------------------------------------------------------+
     *  |                       Tail Counter 2                          |
     *  |                                                               |
     *  +---------------------------------------------------------------+
     *  |                      Active Term Count                        |
     *  +---------------------------------------------------------------+
     *  |                     Cache Line Padding                       ...
     * ...                                                              |
     *  +---------------------------------------------------------------+
     *  |                    End of Stream Position                     |
     *  |                                                               |
     *  +---------------------------------------------------------------+
     *  |                        Is Connected                           |
     *  +---------------------------------------------------------------+
     *  |                      Cache Line Padding                      ...
     * ...                                                              |
     *  +---------------------------------------------------------------+
     *  |                 Registration / Correlation ID                 |
     *  |                                                               |
     *  +---------------------------------------------------------------+
     *  |                        Initial Term Id                        |
     *  +---------------------------------------------------------------+
     *  |                  Default Frame Header Length                  |
     *  +---------------------------------------------------------------+
     *  |                          MTU Length                           |
     *  +---------------------------------------------------------------+
     *  |                         Term Length                           |
     *  +---------------------------------------------------------------+
     *  |                          Page Size                            |
     *  +---------------------------------------------------------------+
     *  |                      Cache Line Padding                      ...
     * ...                                                              |
     *  +---------------------------------------------------------------+
     *  |                     Default Frame Header                     ...
     * ...                                                              |
     *  +---------------------------------------------------------------+
     *

     */
    public static final int LOG_META_DATA_LENGTH;

    static
    {
        int offset = 0;
        TERM_TAIL_COUNTERS_OFFSET = offset;

        offset += (SIZE_OF_LONG * PARTITION_COUNT);
        LOG_ACTIVE_TERM_COUNT_OFFSET = offset;

        offset = (CACHE_LINE_LENGTH * 2);
        LOG_END_OF_STREAM_POSITION_OFFSET = offset;
        LOG_IS_CONNECTED_OFFSET = LOG_END_OF_STREAM_POSITION_OFFSET + SIZE_OF_LONG;

        offset += (CACHE_LINE_LENGTH * 2);
        LOG_CORRELATION_ID_OFFSET = offset;
        LOG_INITIAL_TERM_ID_OFFSET = LOG_CORRELATION_ID_OFFSET + SIZE_OF_LONG;
        LOG_DEFAULT_FRAME_HEADER_LENGTH_OFFSET = LOG_INITIAL_TERM_ID_OFFSET + SIZE_OF_INT;
        LOG_MTU_LENGTH_OFFSET = LOG_DEFAULT_FRAME_HEADER_LENGTH_OFFSET + SIZE_OF_INT;
        LOG_TERM_LENGTH_OFFSET = LOG_MTU_LENGTH_OFFSET + SIZE_OF_INT;
        LOG_PAGE_SIZE_OFFSET = LOG_TERM_LENGTH_OFFSET + SIZE_OF_INT;

        offset += CACHE_LINE_LENGTH;
        LOG_DEFAULT_FRAME_HEADER_OFFSET = offset;

        LOG_META_DATA_LENGTH = align(offset + LOG_DEFAULT_FRAME_HEADER_MAX_LENGTH, PAGE_MIN_SIZE);
    }

    /**
     * Check that term length is valid and alignment is valid.
     *
     * @param termLength to be checked.
     * @throws IllegalStateException if the length is not as expected.
     */
    public static void checkTermLength(final int termLength)
    {
        if (termLength < TERM_MIN_LENGTH)
        {
            throw new IllegalStateException(
                "Term length less than min length of " + TERM_MIN_LENGTH + ": length=" + termLength);
        }

        if (termLength > TERM_MAX_LENGTH)
        {
            throw new IllegalStateException(
                "Term length more than max length of " + TERM_MAX_LENGTH + ": length=" + termLength);
        }

        if (!BitUtil.isPowerOfTwo(termLength))
        {
            throw new IllegalStateException("Term length not a power of 2: length=" + termLength);
        }
    }

    /**
     * Check that page size is valid and alignment is valid.
     *
     * @param pageSize to be checked.
     * @throws IllegalStateException if the size is not as expected.
     */
    public static void checkPageSize(final int pageSize)
    {
        if (pageSize < PAGE_MIN_SIZE)
        {
            throw new IllegalStateException(
                "Page size less than min size of " + PAGE_MIN_SIZE + ": page size=" + pageSize);
        }

        if (pageSize > PAGE_MAX_SIZE)
        {
            throw new IllegalStateException(
                "Page size more than max size of " + PAGE_MAX_SIZE + ": page size=" + pageSize);
        }

        if (!BitUtil.isPowerOfTwo(pageSize))
        {
            throw new IllegalStateException("Page size not a power of 2: page size=" + pageSize);
        }
    }

    /**
     * Get the value of the initial Term id used for this log.
     *
     * @param metadataBuffer containing the meta data.
     * @return the value of the initial Term id used for this log.
     */
    public static int initialTermId(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getInt(LOG_INITIAL_TERM_ID_OFFSET);
    }

    /**
     * Set the initial term at which this log begins. Initial should be randomised so that stream does not get
     * reused accidentally.
     *
     * @param metadataBuffer containing the meta data.
     * @param initialTermId  value to be set.
     */
    public static void initialTermId(final UnsafeBuffer metadataBuffer, final int initialTermId)
    {
        metadataBuffer.putInt(LOG_INITIAL_TERM_ID_OFFSET, initialTermId);
    }

    /**
     * Get the value of the MTU length used for this log.
     *
     * @param metadataBuffer containing the meta data.
     * @return the value of the MTU length used for this log.
     */
    public static int mtuLength(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getInt(LOG_MTU_LENGTH_OFFSET);
    }

    /**
     * Set the MTU length used for this log.
     *
     * @param metadataBuffer containing the meta data.
     * @param mtuLength      value to be set.
     */
    public static void mtuLength(final UnsafeBuffer metadataBuffer, final int mtuLength)
    {
        metadataBuffer.putInt(LOG_MTU_LENGTH_OFFSET, mtuLength);
    }

    /**
     * Get the value of the Term Length used for this log.
     *
     * @param metadataBuffer containing the meta data.
     * @return the value of the term length used for this log.
     */
    public static int termLength(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getInt(LOG_TERM_LENGTH_OFFSET);
    }

    /**
     * Set the term length used for this log.
     *
     * @param metadataBuffer containing the meta data.
     * @param termLength     value to be set.
     */
    public static void termLength(final UnsafeBuffer metadataBuffer, final int termLength)
    {
        metadataBuffer.putInt(LOG_TERM_LENGTH_OFFSET, termLength);
    }

    /**
     * Get the value of the page size used for this log.
     *
     * @param metadataBuffer containing the meta data.
     * @return the value of the page size used for this log.
     */
    public static int pageSize(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getInt(LOG_PAGE_SIZE_OFFSET);
    }

    /**
     * Set the page size used for this log.
     *
     * @param metadataBuffer containing the meta data.
     * @param pageSize       value to be set.
     */
    public static void pageSize(final UnsafeBuffer metadataBuffer, final int pageSize)
    {
        metadataBuffer.putInt(LOG_PAGE_SIZE_OFFSET, pageSize);
    }

    /**
     * Get the value of the correlation ID for this log relating to the command which created it.
     *
     * @param metadataBuffer containing the meta data.
     * @return the value of the correlation ID used for this log.
     */
    public static long correlationId(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getLong(LOG_CORRELATION_ID_OFFSET);
    }

    /**
     * Set the correlation ID used for this log relating to the command which created it.
     *
     * @param metadataBuffer containing the meta data.
     * @param id             value to be set.
     */
    public static void correlationId(final UnsafeBuffer metadataBuffer, final long id)
    {
        metadataBuffer.putLong(LOG_CORRELATION_ID_OFFSET, id);
    }

    /**
     * Get whether the log is considered connected or not by the driver.
     *
     * @param metadataBuffer containing the meta data.
     * @return whether the log is considered connected or not by the driver.
     */
    public static boolean isConnected(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getIntVolatile(LOG_IS_CONNECTED_OFFSET) == 1;
    }

    /**
     * Set whether the log is considered connected or not by the driver.
     *
     * @param metadataBuffer containing the meta data.
     * @param isConnected    or not
     */
    public static void isConnected(final UnsafeBuffer metadataBuffer, final boolean isConnected)
    {
        metadataBuffer.putIntOrdered(LOG_IS_CONNECTED_OFFSET, isConnected ? 1 : 0);
    }

    /**
     * Get the value of the end of stream position.
     *
     * @param metadataBuffer containing the meta data.
     * @return the value of end of stream position
     */
    public static long endOfStreamPosition(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getLongVolatile(LOG_END_OF_STREAM_POSITION_OFFSET);
    }

    /**
     * Set the value of the end of stream position.
     *
     * @param metadataBuffer containing the meta data.
     * @param position       value of the end of stream position
     */
    public static void endOfStreamPosition(final UnsafeBuffer metadataBuffer, final long position)
    {
        metadataBuffer.putLongOrdered(LOG_END_OF_STREAM_POSITION_OFFSET, position);
    }

    /**
     * Get the value of the active term count used by the producer of this log. Consumers may have a different
     * active term count if they are running behind. The read is done with volatile semantics.
     *
     * @param metadataBuffer containing the meta data.
     * @return the value of the active term count used by the producer of this log.
     */
    public static int activeTermCount(final UnsafeBuffer metadataBuffer)
    {
        return metadataBuffer.getIntVolatile(LOG_ACTIVE_TERM_COUNT_OFFSET);
    }

    /**
     * Set the value of the current active term count for the producer using memory ordered semantics.
     *
     * @param metadataBuffer containing the meta data.
     * @param termCount      value of the active term count used by the producer of this log.
     */
    public static void activeTermCountOrdered(final UnsafeBuffer metadataBuffer, final int termCount)
    {
        metadataBuffer.putIntOrdered(LOG_ACTIVE_TERM_COUNT_OFFSET, termCount);
    }

    /**
     * Compare and set the value of the current active term count.
     *
     * @param metadataBuffer    containing the meta data.
     * @param expectedTermCount value of the active term count expected in the log
     * @param updateTermCount   value of the active term count to be updated in the log
     * @return true if successful otherwise false.
     */
    public static boolean casActiveTermCount(
        final UnsafeBuffer metadataBuffer, final int expectedTermCount, final int updateTermCount)
    {
        return metadataBuffer.compareAndSetInt(LOG_ACTIVE_TERM_COUNT_OFFSET, expectedTermCount, updateTermCount);
    }

    /**
     * Set the value of the current active partition index for the producer.
     *
     * @param metadataBuffer containing the meta data.
     * @param termCount      value of the active term count used by the producer of this log.
     */
    public static void activeTermCount(final UnsafeBuffer metadataBuffer, final int termCount)
    {
        metadataBuffer.putInt(LOG_ACTIVE_TERM_COUNT_OFFSET, termCount);
    }

    /**
     * Rotate to the next partition in sequence for the term id.
     *
     * @param currentIndex partition index
     * @return the next partition index
     */
    public static int nextPartitionIndex(final int currentIndex)
    {
        return (currentIndex + 1) % PARTITION_COUNT;
    }

    /**
     * Determine the partition index to be used given the initial term and active term ids.
     *
     * @param initialTermId at which the log buffer usage began
     * @param activeTermId  that is in current usage
     * @return the index of which buffer should be used
     */
    public static int indexByTerm(final int initialTermId, final int activeTermId)
    {
        return (activeTermId - initialTermId) % PARTITION_COUNT;
    }

    /**
     * Determine the partition index based on number of terms that have passed.
     *
     * @param termCount for the number of terms that have passed.
     * @return the partition index for the term count.
     */
    public static int indexByTermCount(final long termCount)
    {
        return (int)(termCount % PARTITION_COUNT);
    }

    /**
     * Determine the partition index given a stream position.
     *
     * @param position            in the stream in bytes.
     * @param positionBitsToShift number of times to right shift the position for term count
     * @return the partition index for the position
     */
    public static int indexByPosition(final long position, final int positionBitsToShift)
    {
        return (int)((position >>> positionBitsToShift) % PARTITION_COUNT);
    }

    /**
     * Compute the current position in absolute number of bytes.
     *
     * @param activeTermId        active term id.
     * @param termOffset          in the term.
     * @param positionBitsToShift number of times to left shift the term count
     * @param initialTermId       the initial term id that this stream started on
     * @return the absolute position in bytes
     */
    public static long computePosition(
        final int activeTermId, final int termOffset, final int positionBitsToShift, final int initialTermId)
    {
        final long termCount = activeTermId - initialTermId; // copes with negative activeTermId on rollover

        return (termCount << positionBitsToShift) + termOffset;
    }

    /**
     * Compute the current position in absolute number of bytes for the beginning of a term.
     *
     * @param activeTermId        active term id.
     * @param positionBitsToShift number of times to left shift the term count
     * @param initialTermId       the initial term id that this stream started on
     * @return the absolute position in bytes
     */
    public static long computeTermBeginPosition(
        final int activeTermId, final int positionBitsToShift, final int initialTermId)
    {
        final long termCount = activeTermId - initialTermId; // copes with negative activeTermId on rollover

        return termCount << positionBitsToShift;
    }

    /**
     * Compute the term id from a position.
     *
     * @param position            to calculate from
     * @param positionBitsToShift number of times to right shift the position
     * @param initialTermId       the initial term id that this stream started on
     * @return the term id according to the position
     */
    public static int computeTermIdFromPosition(
        final long position, final int positionBitsToShift, final int initialTermId)
    {
        return ((int)(position >>> positionBitsToShift) + initialTermId);
    }

    /**
     * Compute the total length of a log file given the term length.
     *
     * Assumes {@link #TERM_MAX_LENGTH} is 1GB and that filePageSize is 1GB or less and a power of 2.
     *
     * @param termLength   on which to base the calculation.
     * @param filePageSize to use for log.
     * @return the total length of the log file.
     */
    public static long computeLogLength(final int termLength, final int filePageSize)
    {
        if (termLength < (1024 * 1024 * 1024))
        {
            return align((termLength * PARTITION_COUNT) + LOG_META_DATA_LENGTH, filePageSize);
        }

        return (PARTITION_COUNT * (long)termLength) + align(LOG_META_DATA_LENGTH, filePageSize);
    }

    /**
     * Store the default frame header to the log meta data buffer.
     *
     * @param metadataBuffer into which the default headers should be stored.
     * @param defaultHeader  to be stored.
     * @throws IllegalArgumentException if the defaultHeader larger than {@link #LOG_DEFAULT_FRAME_HEADER_MAX_LENGTH}
     */
    public static void storeDefaultFrameHeader(final UnsafeBuffer metadataBuffer, final DirectBuffer defaultHeader)
    {
        if (defaultHeader.capacity() != HEADER_LENGTH)
        {
            throw new IllegalArgumentException(
                "Default header length not equal to HEADER_LENGTH: length=" + defaultHeader.capacity());
        }

        metadataBuffer.putInt(LOG_DEFAULT_FRAME_HEADER_LENGTH_OFFSET, HEADER_LENGTH);
        metadataBuffer.putBytes(LOG_DEFAULT_FRAME_HEADER_OFFSET, defaultHeader, 0, HEADER_LENGTH);
    }

    /**
     * Get a wrapper around the default frame header from the log meta data.
     *
     * @param metadataBuffer containing the raw bytes for the default frame header.
     * @return a buffer wrapping the raw bytes.
     */
    public static UnsafeBuffer defaultFrameHeader(final UnsafeBuffer metadataBuffer)
    {
        return new UnsafeBuffer(metadataBuffer, LOG_DEFAULT_FRAME_HEADER_OFFSET, HEADER_LENGTH);
    }

    /**
     * Apply the default header for a message in a term.
     *
     * @param metadataBuffer containing the default headers.
     * @param termBuffer     to which the default header should be applied.
     * @param termOffset     at which the default should be applied.
     */
    public static void applyDefaultHeader(
        final UnsafeBuffer metadataBuffer, final UnsafeBuffer termBuffer, final int termOffset)
    {
        termBuffer.putBytes(termOffset, metadataBuffer, LOG_DEFAULT_FRAME_HEADER_OFFSET, HEADER_LENGTH);
    }

    /**
     * Rotate the log and update the tail counter for the new term.
     *
     * This method is safe for concurrent use.
     *
     * @param metadataBuffer for the log.
     * @param termCount      from which to rotate.
     * @param termId         to be used in the default headers.
     * @return true if log was rotated.
     */
    public static boolean rotateLog(final UnsafeBuffer metadataBuffer, final int termCount, final int termId)
    {
        final int nextTermId = termId + 1;
        final int nextTermCount = termCount + 1;
        final int nextIndex = indexByTermCount(nextTermCount);
        final int expectedTermId = nextTermId - PARTITION_COUNT;

        long rawTail;
        do
        {
            rawTail = rawTail(metadataBuffer, nextIndex);
            if (expectedTermId != termId(rawTail))
            {
                break;
            }
        }
        while (!casRawTail(metadataBuffer, nextIndex, rawTail, packTail(nextTermId, 0)));

        return casActiveTermCount(metadataBuffer, termCount, nextTermCount);
    }

    /**
     * Set the initial value for the termId in the upper bits of the tail counter.
     *
     * @param metadataBuffer contain the tail counter.
     * @param partitionIndex to be initialised.
     * @param termId         to be set.
     */
    public static void initialiseTailWithTermId(
        final UnsafeBuffer metadataBuffer, final int partitionIndex, final int termId)
    {
        metadataBuffer.putLong(TERM_TAIL_COUNTERS_OFFSET + (partitionIndex * SIZE_OF_LONG), packTail(termId, 0));
    }

    /**
     * Get the termId from a packed raw tail value.
     *
     * @param rawTail containing the termId
     * @return the termId from a packed raw tail value.
     */
    public static int termId(final long rawTail)
    {
        return (int)(rawTail >> 32);
    }

    /**
     * Read the termOffset from a packed raw tail value.
     *
     * @param rawTail    containing the termOffset.
     * @param termLength that the offset cannot exceed.
     * @return the termOffset value.
     */
    public static int termOffset(final long rawTail, final long termLength)
    {
        final long tail = rawTail & 0xFFFF_FFFFL;

        return (int)Math.min(tail, termLength);
    }

    /**
     * The termOffset as a result of the append
     *
     * @param result into which the termOffset value has been packed.
     * @return the termOffset after the append
     */
    public static int termOffset(final long result)
    {
        return (int)result;
    }

    /**
     * Pack a termId and termOffset into a raw tail value.
     *
     * @param termId     to be packed.
     * @param termOffset to be packed.
     * @return the packed value.
     */
    public static long packTail(final int termId, final int termOffset)
    {
        return ((long)termId << 32) | (termOffset & 0xFFFF_FFFFL);
    }

    /**
     * Set the raw value of the tail for the given partition.
     *
     * @param metadataBuffer containing the tail counters.
     * @param partitionIndex for the tail counter.
     * @param rawTail        to be stored
     */
    public static void rawTail(final UnsafeBuffer metadataBuffer, final int partitionIndex, final long rawTail)
    {
        metadataBuffer.putLong(TERM_TAIL_COUNTERS_OFFSET + (SIZE_OF_LONG * partitionIndex), rawTail);
    }

    /**
     * Get the raw value of the tail for the given partition.
     *
     * @param metadataBuffer containing the tail counters.
     * @param partitionIndex for the tail counter.
     * @return the raw value of the tail for the current active partition.
     */
    public static long rawTail(final UnsafeBuffer metadataBuffer, final int partitionIndex)
    {
        return metadataBuffer.getLong(TERM_TAIL_COUNTERS_OFFSET + (SIZE_OF_LONG * partitionIndex));
    }

    /**
     * Set the raw value of the tail for the given partition.
     *
     * @param metadataBuffer containing the tail counters.
     * @param partitionIndex for the tail counter.
     * @param rawTail        to be stored
     */
    public static void rawTailVolatile(final UnsafeBuffer metadataBuffer, final int partitionIndex, final long rawTail)
    {
        metadataBuffer.putLongVolatile(TERM_TAIL_COUNTERS_OFFSET + (SIZE_OF_LONG * partitionIndex), rawTail);
    }

    /**
     * Get the raw value of the tail for the given partition.
     *
     * @param metadataBuffer containing the tail counters.
     * @param partitionIndex for the tail counter.
     * @return the raw value of the tail for the current active partition.
     */
    public static long rawTailVolatile(final UnsafeBuffer metadataBuffer, final int partitionIndex)
    {
        return metadataBuffer.getLongVolatile(TERM_TAIL_COUNTERS_OFFSET + (SIZE_OF_LONG * partitionIndex));
    }

    /**
     * Get the raw value of the tail for the current active partition.
     *
     * @param metadataBuffer containing the tail counters.
     * @return the raw value of the tail for the current active partition.
     */
    public static long rawTailVolatile(final UnsafeBuffer metadataBuffer)
    {
        final int partitionIndex = indexByTermCount(activeTermCount(metadataBuffer));
        return metadataBuffer.getLongVolatile(TERM_TAIL_COUNTERS_OFFSET + (SIZE_OF_LONG * partitionIndex));
    }

    /**
     * Compare and set the raw value of the tail for the given partition.
     *
     * @param metadataBuffer  containing the tail counters.
     * @param partitionIndex  for the tail counter.
     * @param expectedRawTail expected current value.
     * @param updateRawTail   to be applied.
     * @return true if the update was successful otherwise false.
     */
    public static boolean casRawTail(
        final UnsafeBuffer metadataBuffer,
        final int partitionIndex,
        final long expectedRawTail,
        final long updateRawTail)
    {
        final int index = TERM_TAIL_COUNTERS_OFFSET + (SIZE_OF_LONG * partitionIndex);
        return metadataBuffer.compareAndSetLong(index, expectedRawTail, updateRawTail);
    }

    /**
     * Get the number of bits to shift when dividing or multiplying by the term buffer length.
     *
     * @param termBufferLength to compute the number of bits to shift for.
     * @return the number of bits to shift to divide or multiply by the term buffer length.
     */
    public static int positionBitsToShift(final int termBufferLength)
    {
        switch (termBufferLength)
        {
            case 64 * 1024:
                return 16;

            case 128 * 1024:
                return 17;

            case 256 * 1024:
                return 18;

            case 512 * 1024:
                return 19;

            case 1024 * 1024:
                return 20;

            case 2 * 1024 * 1024:
                return 21;

            case 4 * 1024 * 1024:
                return 22;

            case 8 * 1024 * 1024:
                return 23;

            case 16 * 1024 * 1024:
                return 24;

            case 32 * 1024 * 1024:
                return 25;

            case 64 * 1024 * 1024:
                return 26;

            case 128 * 1024 * 1024:
                return 27;

            case 256 * 1024 * 1024:
                return 28;

            case 512 * 1024 * 1024:
                return 29;

            case 1024 * 1024 * 1024:
                return 30;
        }

        throw new IllegalArgumentException("Invalid term buffer length: " + termBufferLength);
    }
}