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

import io.aeron.Aeron;
import io.aeron.ChannelUri;
import io.aeron.Publication;
import io.aeron.cluster.client.ClusterException;
import org.agrona.CloseHelper;
import org.agrona.collections.ArrayUtil;
import org.agrona.collections.Int2ObjectHashMap;

import static io.aeron.Aeron.NULL_VALUE;
import static io.aeron.CommonContext.ENDPOINT_PARAM_NAME;
import static io.aeron.archive.client.AeronArchive.NULL_POSITION;

/**
 * Represents a member of the cluster that participates in consensus.
 */
public final class ClusterMember
{
    public static final ClusterMember[] EMPTY_CLUSTER_MEMBER_ARRAY = new ClusterMember[0];

    private boolean isBallotSent;
    private boolean isLeader;
    private boolean hasRequestedJoin;
    private boolean hasSentTerminationAck;
    private int id;
    private long leadershipTermId = Aeron.NULL_VALUE;
    private long logPosition = NULL_POSITION;
    private long candidateTermId = Aeron.NULL_VALUE;
    private long catchupReplaySessionId = Aeron.NULL_VALUE;
    private long changeCorrelationId = Aeron.NULL_VALUE;
    private long removalPosition = NULL_POSITION;
    private long timeOfLastAppendPositionMs = Aeron.NULL_VALUE;
    private final String clientFacingEndpoint;
    private final String memberFacingEndpoint;
    private final String logEndpoint;
    private final String transferEndpoint;
    private final String archiveEndpoint;
    private final String endpointsDetail;
    private Publication publication;
    private Boolean vote = null;

    /**
     * Construct a new member of the cluster.
     *
     * @param id                   unique id for the member.
     * @param clientFacingEndpoint address and port endpoint to which cluster clients connect.
     * @param memberFacingEndpoint address and port endpoint to which other cluster members connect.
     * @param logEndpoint          address and port endpoint to which the log is replicated.
     * @param transferEndpoint     address and port endpoint to which a stream is replayed to catchup to the leader.
     * @param archiveEndpoint      address and port endpoint to which the archive control channel can be reached.
     * @param endpointsDetail      comma separated list of endpoints.
     */
    public ClusterMember(
        final int id,
        final String clientFacingEndpoint,
        final String memberFacingEndpoint,
        final String logEndpoint,
        final String transferEndpoint,
        final String archiveEndpoint,
        final String endpointsDetail)
    {
        this.id = id;
        this.clientFacingEndpoint = clientFacingEndpoint;
        this.memberFacingEndpoint = memberFacingEndpoint;
        this.logEndpoint = logEndpoint;
        this.transferEndpoint = transferEndpoint;
        this.archiveEndpoint = archiveEndpoint;
        this.endpointsDetail = endpointsDetail;
    }

    /**
     * Reset the state of a cluster member so it can be canvassed and reestablished.
     */
    public void reset()
    {
        isBallotSent = false;
        isLeader = false;
        hasRequestedJoin = false;
        hasSentTerminationAck = false;
        vote = null;
        candidateTermId = Aeron.NULL_VALUE;
        leadershipTermId = Aeron.NULL_VALUE;
        logPosition = NULL_POSITION;
    }

    /**
     * Set if this member should be leader.
     *
     * @param isLeader value.
     * @return this for a fluent API.
     */
    public ClusterMember isLeader(final boolean isLeader)
    {
        this.isLeader = isLeader;
        return this;
    }

    /**
     * Is this member currently the leader?
     *
     * @return true if this member is currently the leader otherwise false.
     */
    public boolean isLeader()
    {
        return isLeader;
    }

    /**
     * Is the ballot for the current election sent to this member?
     *
     * @param isBallotSent is the ballot for the current election sent to this member?
     * @return this for a fluent API.
     */
    public ClusterMember isBallotSent(final boolean isBallotSent)
    {
        this.isBallotSent = isBallotSent;
        return this;
    }

    /**
     * Is the ballot for the current election sent to this member?
     *
     * @return true if the ballot has been sent for this member in the current election.
     */
    public boolean isBallotSent()
    {
        return isBallotSent;
    }

    /**
     * Set if this member requested to join the cluster.
     *
     * @param hasRequestedJoin the cluster.
     * @return this for a fluent API.
     */
    public ClusterMember hasRequestedJoin(final boolean hasRequestedJoin)
    {
        this.hasRequestedJoin = hasRequestedJoin;
        return this;
    }

    /**
     * Has this member requested to join the cluster?
     *
     * @return has this member requested to join the cluster?
     */
    public boolean hasRequestedJoin()
    {
        return hasRequestedJoin;
    }

    /**
     * Set if this member has sent a termination ack.
     *
     * @param hasSentTerminationAck to the leader.
     * @return this for a fluent API.
     */
    public ClusterMember hasSentTerminationAck(final boolean hasSentTerminationAck)
    {
        this.hasSentTerminationAck = hasSentTerminationAck;
        return this;
    }

    /**
     * Has this member sent a termination ack?
     *
     * @return has this member sent a termination ack?
     */
    public boolean hasSentTerminationAck()
    {
        return hasSentTerminationAck;
    }

    /**
     * Set the log position as of appending the event to be removed from the cluster.
     *
     * @param removalPosition as of appending the event to be removed from the cluster.
     * @return this for a fluent API.
     */
    public ClusterMember removalPosition(final long removalPosition)
    {
        this.removalPosition = removalPosition;
        return this;
    }

    /**
     * The log position as of appending the event to be removed from the cluster.
     *
     * @return the log position as of appending the event to be removed from the cluster,
     * or {@link io.aeron.archive.client.AeronArchive#NULL_POSITION} if not requested remove.
     */
    public long removalPosition()
    {
        return removalPosition;
    }

    /**
     * Has this member requested to be removed from the cluster.
     *
     * @return true if this member requested to be removed from the cluster, otherwise false.
     */
    public boolean hasRequestedRemove()
    {
        return removalPosition != NULL_POSITION;
    }

    /**
     * Set the unique id for this member of the cluster.
     *
     * @param id for this member of the cluster.
     * @return this for a fluent API.
     */
    public ClusterMember id(final int id)
    {
        this.id = id;
        return this;
    }

    /**
     * Unique identity for this member in the cluster.
     *
     * @return the unique identity for this member in the cluster.
     */
    public int id()
    {
        return id;
    }

    /**
     * Set the result of the vote for this member. {@link Boolean#TRUE} means they voted for this member,
     * {@link Boolean#FALSE} means they voted against this member, and null means no vote was received.
     *
     * @param vote for this member in the election.
     * @return this for a fluent API.
     */
    public ClusterMember vote(final Boolean vote)
    {
        this.vote = vote;
        return this;
    }

    /**
     * The status of the vote for this member in an election. {@link Boolean#TRUE} means they voted for this member,
     * {@link Boolean#FALSE} means they voted against this member, and null means no vote was received.
     *
     * @return the status of the vote for this member in an election.
     */
    public Boolean vote()
    {
        return vote;
    }

    /**
     * The leadership term reached for the cluster member.
     *
     * @param leadershipTermId leadership term reached for the cluster member.
     * @return this for a fluent API.
     */
    public ClusterMember leadershipTermId(final long leadershipTermId)
    {
        this.leadershipTermId = leadershipTermId;
        return this;
    }

    /**
     * The leadership term reached for the cluster member.
     *
     * @return The leadership term reached for the cluster member.
     */
    public long leadershipTermId()
    {
        return leadershipTermId;
    }

    /**
     * The log position this member has persisted.
     *
     * @param logPosition this member has persisted.
     * @return this for a fluent API.
     */
    public ClusterMember logPosition(final long logPosition)
    {
        this.logPosition = logPosition;
        return this;
    }

    /**
     * The log position this member has persisted.
     *
     * @return the log position this member has persisted.
     */
    public long logPosition()
    {
        return logPosition;
    }

    /**
     * The candidate term id used when voting.
     *
     * @param candidateTermId used when voting.
     * @return this for a fluent API.
     */
    public ClusterMember candidateTermId(final long candidateTermId)
    {
        this.candidateTermId = candidateTermId;
        return this;
    }

    /**
     * The candidate term id used when voting.
     *
     * @return the candidate term id used when voting.
     */
    public long candidateTermId()
    {
        return candidateTermId;
    }

    /**
     * The session id for the replay when catching up to the leader.
     *
     * @param replaySessionId for the replay when catching up to the leader.
     * @return this for a fluent API.
     */
    public ClusterMember catchupReplaySessionId(final long replaySessionId)
    {
        this.catchupReplaySessionId = replaySessionId;
        return this;
    }

    /**
     * The session id for the replay when catching up to the leader.
     *
     * @return the session id for the replay when catching up to the leader.
     */
    public long catchupReplaySessionId()
    {
        return catchupReplaySessionId;
    }

    /**
     * Correlation id assigned to the current action undertaken by the cluster member.
     *
     * @param correlationId assigned to the current action undertaken by the cluster member.
     * @return this for a fluent API.
     */
    public ClusterMember correlationId(final long correlationId)
    {
        this.changeCorrelationId = correlationId;
        return this;
    }

    /**
     * Correlation id assigned to the current action undertaken by the cluster member.
     *
     * @return correlation id assigned to the current action undertaken by the cluster member.
     */
    public long correlationId()
    {
        return changeCorrelationId;
    }

    /**
     * Time (in ms) of last received appendPosition.
     *
     * @param timeMs of the last received appendPosition
     * @return this for a fluent API.
     */
    public ClusterMember timeOfLastAppendPositionMs(final long timeMs)
    {
        this.timeOfLastAppendPositionMs = timeMs;
        return this;
    }

    /**
     * Time (in ms) of last received appendPosition.
     *
     * @return time (in ms) of last received appendPosition or {@link Aeron#NULL_VALUE} if none received.
     */
    public long timeOfLastAppendPositionMs()
    {
        return timeOfLastAppendPositionMs;
    }

    /**
     * The address:port endpoint for this cluster member that clients will connect to.
     *
     * @return the address:port endpoint for this cluster member that clients will connect to.
     */
    public String clientFacingEndpoint()
    {
        return clientFacingEndpoint;
    }

    /**
     * The address:port endpoint for this cluster member that other members connect to.
     *
     * @return the address:port endpoint for this cluster member that other members will connect to.
     */
    public String memberFacingEndpoint()
    {
        return memberFacingEndpoint;
    }

    /**
     * The address:port endpoint for this cluster member that the log is replicated to.
     *
     * @return the address:port endpoint for this cluster member that the log is replicated to.
     */
    public String logEndpoint()
    {
        return logEndpoint;
    }

    /**
     * The address:port endpoint for this cluster member to which a stream is replayed to for leader catchup.
     *
     * @return the address:port endpoint for this cluster member to which a stream is replayed to for leader catchup.
     */
    public String transferEndpoint()
    {
        return transferEndpoint;
    }

    /**
     * The address:port endpoint for this cluster member that the archive can be reached.
     *
     * @return the address:port endpoint for this cluster member that the archive can be reached.
     */
    public String archiveEndpoint()
    {
        return archiveEndpoint;
    }

    /**
     * The string of endpoints for this member in a comma separated list in the same order they are parsed.
     *
     * @return list of endpoints for this member in a comma separated list.
     * @see #parse(String)
     */
    public String endpointsDetail()
    {
        return endpointsDetail;
    }

    /**
     * The {@link Publication} used for send status updates to the member.
     *
     * @return {@link Publication} used for send status updates to the member.
     */
    public Publication publication()
    {
        return publication;
    }

    /**
     * {@link Publication} used for send status updates to the member.
     *
     * @param publication used for send status updates to the member.
     */
    public void publication(final Publication publication)
    {
        this.publication = publication;
    }

    /**
     * Close member status publication and null out reference.
     */
    public void closePublication()
    {
        CloseHelper.close(publication);
        publication = null;
    }

    /**
     * Parse the details for a cluster members from a string.
     *

     *
     * member-id,client-facing:port,member-facing:port,log:port,transfer:port,archive:port|1,...
     *
     *
     * @param value of the string to be parsed.
     * @return An array of cluster members.
     */
    public static ClusterMember[] parse(final String value)
    {
        if (null == value || value.length() == 0)
        {
            return ClusterMember.EMPTY_CLUSTER_MEMBER_ARRAY;
        }

        final String[] memberValues = value.split("\\|");
        final int length = memberValues.length;
        final ClusterMember[] members = new ClusterMember[length];

        for (int i = 0; i < length; i++)
        {
            final String endpointsDetail = memberValues[i];
            final String[] memberAttributes = endpointsDetail.split(",");
            if (memberAttributes.length != 6)
            {
                throw new ClusterException("invalid member value: " + endpointsDetail + " within: " + value);
            }

            final String justEndpoints = String.join(
                ",",
                memberAttributes[1],
                memberAttributes[2],
                memberAttributes[3],
                memberAttributes[4],
                memberAttributes[5]);

            members[i] = new ClusterMember(
                Integer.parseInt(memberAttributes[0]),
                memberAttributes[1],
                memberAttributes[2],
                memberAttributes[3],
                memberAttributes[4],
                memberAttributes[5],
                justEndpoints);
        }

        return members;
    }

    public static ClusterMember parseEndpoints(final int id, final String endpointsDetail)
    {
        final String[] memberAttributes = endpointsDetail.split(",");
        if (memberAttributes.length != 5)
        {
            throw new ClusterException("invalid member value: " + endpointsDetail);
        }

        return new ClusterMember(
            id,
            memberAttributes[0],
            memberAttributes[1],
            memberAttributes[2],
            memberAttributes[3],
            memberAttributes[4],
            endpointsDetail);
    }

    /**
     * Encode member details from a cluster members array to a string.
     *
     * @param clusterMembers to fill the details from
     * @return String representation suitable for use with {@link ClusterMember#parse}
     */
    public static String encodeAsString(final ClusterMember[] clusterMembers)
    {
        final StringBuilder builder = new StringBuilder();

        for (int i = 0, length = clusterMembers.length; i < length; i++)
        {
            final ClusterMember member = clusterMembers[i];

            builder
                .append(member.id())
                .append(',')
                .append(member.endpointsDetail());

            if ((length - 1) != i)
            {
                builder.append('|');
            }
        }

        return builder.toString();
    }

    /**
     * Add the publications for sending status messages to the other members of the cluster.
     *
     * @param members    of the cluster.
     * @param exclude    this member when adding publications.
     * @param channelUri for the publication.
     * @param streamId   for the publication.
     * @param aeron      to add the publications to.
     */
    public static void addMemberStatusPublications(
        final ClusterMember[] members,
        final ClusterMember exclude,
        final ChannelUri channelUri,
        final int streamId,
        final Aeron aeron)
    {
        for (final ClusterMember member : members)
        {
            if (member != exclude)
            {
                channelUri.put(ENDPOINT_PARAM_NAME, member.memberFacingEndpoint());
                member.publication = aeron.addExclusivePublication(channelUri.toString(), streamId);
            }
        }
    }

    /**
     * Close the publications associated with members of the cluster.
     *
     * @param clusterMembers to close the publications for.
     */
    public static void closeMemberPublications(final ClusterMember[] clusterMembers)
    {
        for (final ClusterMember member : clusterMembers)
        {
            CloseHelper.close(member.publication);
        }
    }

    /**
     * Add an exclusive {@link Publication} for communicating to a member on the member status channel.
     *
     * @param member     to which the publication is addressed.
     * @param channelUri for the target member.
     * @param streamId   for the target member.
     * @param aeron      from which the publication will be created.
     */
    public static void addMemberStatusPublication(
        final ClusterMember member, final ChannelUri channelUri, final int streamId, final Aeron aeron)
    {
        channelUri.put(ENDPOINT_PARAM_NAME, member.memberFacingEndpoint());
        member.publication = aeron.addExclusivePublication(channelUri.toString(), streamId);
    }

    /**
     * Populate map of {@link ClusterMember}s which can be looked up by id.
     *
     * @param clusterMembers       to populate the map.
     * @param clusterMemberByIdMap to be populated.
     */
    public static void addClusterMemberIds(
        final ClusterMember[] clusterMembers, final Int2ObjectHashMap<ClusterMember> clusterMemberByIdMap)
    {
        for (final ClusterMember member : clusterMembers)
        {
            clusterMemberByIdMap.put(member.id(), member);
        }
    }

    /**
     * Check if the cluster leader has an active quorum of cluster followers.
     *
     * @param clusterMembers for the current cluster.
     * @param nowMs          for the current time.
     * @param timeoutMs      after which a follower is not considered active.
     * @return true if quorum of cluster members are considered active.
     */
    public static boolean hasActiveQuorum(
        final ClusterMember[] clusterMembers, final long nowMs, final long timeoutMs)
    {
        int threshold = quorumThreshold(clusterMembers.length);

        for (final ClusterMember member : clusterMembers)
        {
            if (member.isLeader() || nowMs <= (member.timeOfLastAppendPositionMs() + timeoutMs))
            {
                if (--threshold <= 0)
                {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * The threshold of clusters members required to achieve quorum given a count of cluster members.
     *
     * @param memberCount for the cluster
     * @return the threshold for achieving quorum.
     */
    public static int quorumThreshold(final int memberCount)
    {
        return (memberCount / 2) + 1;
    }

    /**
     * Calculate the position reached by a quorum of cluster members.
     *
     * @param members         of the cluster.
     * @param rankedPositions temp array to be used for sorting the positions to avoid allocation.
     * @return the position reached by a quorum of cluster members.
     */
    public static long quorumPosition(final ClusterMember[] members, final long[] rankedPositions)
    {
        final int length = rankedPositions.length;
        for (int i = 0; i < length; i++)
        {
            rankedPositions[i] = 0;
        }

        for (final ClusterMember member : members)
        {
            long newPosition = member.logPosition;

            for (int i = 0; i < length; i++)
            {
                final long rankedPosition = rankedPositions[i];

                if (newPosition > rankedPosition)
                {
                    rankedPositions[i] = newPosition;
                    newPosition = rankedPosition;
                }
            }
        }

        return rankedPositions[length - 1];
    }

    /**
     * Reset the log position of all the members to the provided value.
     *
     * @param clusterMembers to be reset.
     * @param logPosition    to set for them all.
     */
    public static void resetLogPositions(final ClusterMember[] clusterMembers, final long logPosition)
    {
        for (final ClusterMember member : clusterMembers)
        {
            member.logPosition(logPosition);
        }
    }

    /**
     * Has the members of the cluster the voted reached the provided position in their log.
     *
     * @param clusterMembers   to check.
     * @param position         to compare the {@link #logPosition()} against.
     * @param leadershipTermId expected of the members.
     * @return true if all members have reached this position otherwise false.
     */
    public static boolean haveVotersReachedPosition(
        final ClusterMember[] clusterMembers, final long position, final long leadershipTermId)
    {
        for (final ClusterMember member : clusterMembers)
        {
            if (member.vote != null && (member.logPosition < position || member.leadershipTermId != leadershipTermId))
            {
                return false;
            }
        }

        return true;
    }

    /**
     * Reset the state of all cluster members.
     *
     * @param members to reset.
     */
    public static void reset(final ClusterMember[] members)
    {
        for (final ClusterMember member : members)
        {
            member.reset();
        }
    }

    /**
     * Become a candidate by voting for yourself and resetting the other votes to {@link Aeron#NULL_VALUE}.
     *
     * @param members           to reset the votes for.
     * @param candidateTermId   for the candidacy.
     * @param candidateMemberId for the election.
     */
    public static void becomeCandidate(
        final ClusterMember[] members, final long candidateTermId, final int candidateMemberId)
    {
        for (final ClusterMember member : members)
        {
            if (member.id == candidateMemberId)
            {
                member.vote(Boolean.TRUE)
                    .candidateTermId(candidateTermId)
                    .isBallotSent(true);
            }
            else
            {
                member.vote(null)
                    .candidateTermId(Aeron.NULL_VALUE)
                    .isBallotSent(false);
            }
        }
    }

    /**
     * Has the candidate got unanimous support of the cluster?
     *
     * @param members         to check for votes.
     * @param candidateTermId for the vote.
     * @return false if any member has not voted for the candidate.
     */
    public static boolean hasWonVoteOnFullCount(final ClusterMember[] members, final long candidateTermId)
    {
        int votes = 0;

        for (final ClusterMember member : members)
        {
            if (null == member.vote || member.candidateTermId != candidateTermId)
            {
                return false;
            }

            votes += member.vote ? 1 : 0;
        }

        return votes >= ClusterMember.quorumThreshold(members.length);
    }

    /**
     * Has sufficient votes being counted for a majority for all members observed during {@link Election.State#CANVASS}?
     *
     * @param members         to check for votes.
     * @param candidateTermId for the vote.
     * @return false if any member has not voted for the candidate.
     */
    public static boolean hasMajorityVoteWithCanvassMembers(final ClusterMember[] members, final long candidateTermId)
    {
        int votes = 0;
        for (final ClusterMember member : members)
        {
            if (NULL_POSITION != member.logPosition && null == member.vote)
            {
                return false;
            }

            if (Boolean.TRUE.equals(member.vote) && member.candidateTermId == candidateTermId)
            {
                ++votes;
            }
        }

        return votes >= ClusterMember.quorumThreshold(members.length);
    }

    /**
     * Has sufficient votes being counted for a majority?
     *
     * @param clusterMembers  to check for votes.
     * @param candidateTermId for the vote.
     * @return true if a majority of positive votes.
     */
    public static boolean hasMajorityVote(final ClusterMember[] clusterMembers, final long candidateTermId)
    {
        int votes = 0;
        for (final ClusterMember member : clusterMembers)
        {
            if (Boolean.TRUE.equals(member.vote) && member.candidateTermId == candidateTermId)
            {
                ++votes;
            }
        }

        return votes >= ClusterMember.quorumThreshold(clusterMembers.length);
    }

    /**
     * Determine which member of a cluster this is and check endpoints.
     *
     * @param clusterMembers  for the current cluster which can be null.
     * @param memberId        for this member.
     * @param memberEndpoints for this member.
     * @return the {@link ClusterMember} determined.
     */
    public static ClusterMember determineMember(
        final ClusterMember[] clusterMembers, final int memberId, final String memberEndpoints)
    {
        ClusterMember member = NULL_VALUE != memberId ? ClusterMember.findMember(clusterMembers, memberId) : null;

        if ((null == clusterMembers || 0 == clusterMembers.length) && null == member)
        {
            member = ClusterMember.parseEndpoints(NULL_VALUE, memberEndpoints);
        }
        else
        {
            if (null == member)
            {
                throw new ClusterException("memberId=" + memberId + " not found in clusterMembers");
            }

            if (!"".equals(memberEndpoints))
            {
                ClusterMember.validateMemberEndpoints(member, memberEndpoints);
            }
        }

        return member;
    }

    /**
     * Check the member with the memberEndpoints
     *
     * @param member          to check memberEndpoints against
     * @param memberEndpoints to check member against
     * @see ConsensusModule.Context#memberEndpoints()
     * @see ConsensusModule.Context#clusterMembers()
     */
    public static void validateMemberEndpoints(final ClusterMember member, final String memberEndpoints)
    {
        final ClusterMember endpointMember = ClusterMember.parseEndpoints(Aeron.NULL_VALUE, memberEndpoints);

        if (!areSameEndpoints(member, endpointMember))
        {
            throw new ClusterException(
                "clusterMembers and memberEndpoints differ: " + member.endpointsDetail() + " != " + memberEndpoints);
        }
    }

    /**
     * Are two cluster members using the same endpoints?
     *
     * @param lhs to check
     * @param rhs to check
     * @return true if both are using the same endpoints or false if not.
     */
    public static boolean areSameEndpoints(final ClusterMember lhs, final ClusterMember rhs)
    {
        return lhs.clientFacingEndpoint().equals(rhs.clientFacingEndpoint()) &&
            lhs.memberFacingEndpoint().equals(rhs.memberFacingEndpoint()) &&
            lhs.logEndpoint().equals(rhs.logEndpoint()) &&
            lhs.transferEndpoint().equals(rhs.transferEndpoint()) &&
            lhs.archiveEndpoint().equals(rhs.archiveEndpoint());
    }

    /**
     * Has the member achieved a unanimous view to be a suitable candidate in an election.
     *
     * @param clusterMembers to compare the candidate against.
     * @param candidate      for leadership.
     * @return true if the candidate is suitable otherwise false.
     */
    public static boolean isUnanimousCandidate(final ClusterMember[] clusterMembers, final ClusterMember candidate)
    {
        for (final ClusterMember member : clusterMembers)
        {
            if (NULL_POSITION == member.logPosition || compareLog(candidate, member) < 0)
            {
                return false;
            }
        }

        return true;
    }

    /**
     * Has the member achieved a quorum view to be a suitable candidate in an election.
     *
     * @param clusterMembers to compare the candidate against.
     * @param candidate      for leadership.
     * @return true if the candidate is suitable otherwise false.
     */
    public static boolean isQuorumCandidate(final ClusterMember[] clusterMembers, final ClusterMember candidate)
    {
        int possibleVotes = 0;
        for (final ClusterMember member : clusterMembers)
        {
            if (NULL_POSITION == member.logPosition || compareLog(candidate, member) < 0)
            {
                continue;
            }

            ++possibleVotes;
        }

        return possibleVotes >= ClusterMember.quorumThreshold(clusterMembers.length);
    }

    /**
     * The result is positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more
     * recent log.
     *
     * @param lhsLogLeadershipTermId term for which the position is most recent.
     * @param lhsLogPosition         reached in the provided term.
     * @param rhsLogLeadershipTermId term for which the position is most recent.
     * @param rhsLogPosition         reached in the provided term.
     * @return positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more
     * recent log.
     */
    public static int compareLog(
        final long lhsLogLeadershipTermId,
        final long lhsLogPosition,
        final long rhsLogLeadershipTermId,
        final long rhsLogPosition)
    {
        if (lhsLogLeadershipTermId > rhsLogLeadershipTermId)
        {
            return 1;
        }
        else if (lhsLogLeadershipTermId < rhsLogLeadershipTermId)
        {
            return -1;
        }
        else if (lhsLogPosition > rhsLogPosition)
        {
            return 1;
        }
        else if (lhsLogPosition < rhsLogPosition)
        {
            return -1;
        }

        return 0;
    }

    /**
     * The result is positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more
     * recent log.
     *
     * @param lhs member to compare.
     * @param rhs member to compare.
     * @return positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more
     * recent log.
     */
    public static int compareLog(final ClusterMember lhs, final ClusterMember rhs)
    {
        return compareLog(lhs.leadershipTermId, lhs.logPosition, rhs.leadershipTermId, rhs.logPosition);
    }

    /**
     * Is the string of member endpoints not duplicated in the members.
     *
     * @param members         to check if the provided endpoints have a duplicate.
     * @param memberEndpoints to check for duplicates.
     * @return true if no duplicate is found otherwise false.
     */
    public static boolean isNotDuplicateEndpoints(final ClusterMember[] members, final String memberEndpoints)
    {
        for (final ClusterMember member : members)
        {
            if (member.endpointsDetail().equals(memberEndpoints))
            {
                return false;
            }
        }

        return true;
    }

    /**
     * Find the index at which a member id is present.
     *
     * @param clusterMembers to be searched.
     * @param memberId       to search for.
     * @return the index at which the member id is found otherwise {@link ArrayUtil#UNKNOWN_INDEX}.
     */
    public static int findMemberIndex(final ClusterMember[] clusterMembers, final int memberId)
    {
        final int length = clusterMembers.length;
        int index = ArrayUtil.UNKNOWN_INDEX;

        for (int i = 0; i < length; i++)
        {
            if (clusterMembers[i].id() == memberId)
            {
                index = i;
            }
        }

        return index;
    }

    /**
     * Find a {@link ClusterMember} with a given id.
     *
     * @param clusterMembers to search.
     * @param memberId       to search for.
     * @return the {@link ClusterMember} if found otherwise null.
     */
    public static ClusterMember findMember(final ClusterMember[] clusterMembers, final int memberId)
    {
        for (final ClusterMember member : clusterMembers)
        {
            if (member.id() == memberId)
            {
                return member;
            }
        }

        return null;
    }

    /**
     * Add a new member to an array of {@link ClusterMember}s.
     *
     * @param oldMembers to add to.
     * @param newMember  to add.
     * @return a new array containing the old members plus the new member.
     */
    public static ClusterMember[] addMember(final ClusterMember[] oldMembers, final ClusterMember newMember)
    {
        return ArrayUtil.add(oldMembers, newMember);
    }

    /**
     * Remove a member from an array if found, otherwise return the array unmodified.
     *
     * @param oldMembers to remove a member from.
     * @param memberId   of the member to remove.
     * @return a new array with the member removed or the existing array if not found.
     */
    public static ClusterMember[] removeMember(final ClusterMember[] oldMembers, final int memberId)
    {
        return ArrayUtil.remove(oldMembers, findMemberIndex(oldMembers, memberId));
    }

    /**
     * Find the highest member id in an array of members.
     *
     * @param clusterMembers to search for the highest id.
     * @return the highest id otherwise {@link Aeron#NULL_VALUE} if empty.
     */
    public static int highMemberId(final ClusterMember[] clusterMembers)
    {
        int highId = Aeron.NULL_VALUE;

        for (final ClusterMember member : clusterMembers)
        {
            highId = Math.max(highId, member.id());
        }

        return highId;
    }

    /**
     * Create a string of member facing endpoints by id in format {@code id=endpoint,id=endpoint, ...}.
     *
     * @param members for which the endpoints string will be generated.
     * @return  a string of member facing endpoints by id.
     */
    public static String clientFacingEndpoints(final ClusterMember[] members)
    {
        final StringBuilder builder = new StringBuilder(100);

        for (int i = 0, length = members.length; i < length; i++)
        {
            if (0 != i)
            {
                builder.append(',');
            }

            final ClusterMember member = members[i];
            builder.append(member.id()).append('=').append(member.clientFacingEndpoint());
        }

        return builder.toString();
    }

    public String toString()
    {
        return "ClusterMember{" +
            "isBallotSent=" + isBallotSent +
            ", isLeader=" + isLeader +
            ", hasRequestedJoin=" + hasRequestedJoin +
            ", id=" + id +
            ", leadershipTermId=" + leadershipTermId +
            ", logPosition=" + logPosition +
            ", candidateTermId=" + candidateTermId +
            ", catchupReplaySessionId=" + catchupReplaySessionId +
            ", correlationId=" + changeCorrelationId +
            ", removalPosition=" + removalPosition +
            ", timeOfLastAppendPositionMs=" + timeOfLastAppendPositionMs +
            ", clientFacingEndpoint='" + clientFacingEndpoint + '\'' +
            ", memberFacingEndpoint='" + memberFacingEndpoint + '\'' +
            ", logEndpoint='" + logEndpoint + '\'' +
            ", transferEndpoint='" + transferEndpoint + '\'' +
            ", archiveEndpoint='" + archiveEndpoint + '\'' +
            ", endpointsDetail='" + endpointsDetail + '\'' +
            ", publication=" + publication +
            ", vote=" + vote +
            '}';
    }
}


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

import io.aeron.protocol.StatusMessageFlyweight;
import org.agrona.collections.ArrayListUtil;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

import static io.aeron.logbuffer.LogBufferDescriptor.computePosition;
import static org.agrona.SystemUtil.getDurationInNanos;

/**
 * Minimum multicast sender flow control strategy.
 *

 * Flow control is set to minimum of tracked receivers.
 *

 * Tracking of receivers is done as long as they continue to send Status Messages. Once SMs stop, the receiver tracking
 * for that receiver will timeout after a given number of nanoseconds.
 */
public class MinMulticastFlowControl implements FlowControl
{
    /**
     * Property name to set timeout, in nanoseconds, for a receiver to be tracked.
     */
    public static final String RECEIVER_TIMEOUT_PROP_NAME = "aeron.MinMulticastFlowControl.receiverTimeout";

    /**
     * Default timeout, in nanoseconds, until a receiver is no longer tracked and considered for minimum.
     */
    public static final long RECEIVER_TIMEOUT_DEFAULT = TimeUnit.SECONDS.toNanos(2);

    public static final long RECEIVER_TIMEOUT = getDurationInNanos(
        RECEIVER_TIMEOUT_PROP_NAME, RECEIVER_TIMEOUT_DEFAULT);

    private final ArrayList<Receiver> receiverList = new ArrayList<>();
    private volatile boolean shouldLinger = true;

    /**
     * {@inheritDoc}
     */
    public void initialize(final int initialTermId, final int termBufferLength)
    {
    }

    /**
     * {@inheritDoc}
     */
    public long onStatusMessage(
        final StatusMessageFlyweight flyweight,
        final InetSocketAddress receiverAddress,
        final long senderLimit,
        final int initialTermId,
        final int positionBitsToShift,
        final long timeNs)
    {
        final long position = computePosition(
            flyweight.consumptionTermId(),
            flyweight.consumptionTermOffset(),
            positionBitsToShift,
            initialTermId);

        final long windowLength = flyweight.receiverWindowLength();
        final long receiverId = flyweight.receiverId();
        boolean isExisting = false;
        long minPosition = Long.MAX_VALUE;

        final ArrayList<Receiver> receiverList = this.receiverList;
        for (int i = 0, size = receiverList.size(); i < size; i++)
        {
            final Receiver receiver = receiverList.get(i);

            if (receiverId == receiver.receiverId)
            {
                receiver.lastPosition = Math.max(position, receiver.lastPosition);
                receiver.lastPositionPlusWindow = position + windowLength;
                receiver.timeOfLastStatusMessageNs = timeNs;
                isExisting = true;
            }

            minPosition = Math.min(minPosition, receiver.lastPositionPlusWindow);
        }

        if (!isExisting)
        {
            receiverList.add(new Receiver(
                position, position + windowLength, timeNs, receiverId, receiverAddress));
            minPosition = Math.min(minPosition, position + windowLength);
        }

        return Math.max(senderLimit, minPosition);
    }

    /**
     * {@inheritDoc}
     */
    public long onIdle(final long timeNs, final long senderLimit, final long senderPosition, final boolean isEos)
    {
        long minPosition = Long.MAX_VALUE;
        long minLimitPosition = Long.MAX_VALUE;
        final ArrayList<Receiver> receiverList = this.receiverList;

        for (int lastIndex = receiverList.size() - 1, i = lastIndex; i >= 0; i--)
        {
            final Receiver receiver = receiverList.get(i);
            if ((receiver.timeOfLastStatusMessageNs + RECEIVER_TIMEOUT) - timeNs < 0)
            {
                ArrayListUtil.fastUnorderedRemove(receiverList, i, lastIndex--);
            }
            else
            {
                minPosition = Math.min(minPosition, receiver.lastPosition);
                minLimitPosition = Math.min(minLimitPosition, receiver.lastPositionPlusWindow);
            }
        }

        if (isEos && shouldLinger)
        {
            if (0 == receiverList.size() || minPosition >= senderPosition)
            {
                shouldLinger = false;
            }
        }

        return receiverList.size() > 0 ? minLimitPosition : senderLimit;
    }

    /**
     * {@inheritDoc}
     */
    public boolean shouldLinger(final long timeNs)
    {
        return shouldLinger;
    }

    static class Receiver
    {
        long lastPosition;
        long lastPositionPlusWindow;
        long timeOfLastStatusMessageNs;
        final long receiverId;
        final InetSocketAddress address;

        Receiver(
            final long lastPosition,
            final long lastPositionPlusWindow,
            final long timeNs,
            final long receiverId,
            final InetSocketAddress receiverAddress)
        {
            this.lastPosition = lastPosition;
            this.lastPositionPlusWindow = lastPositionPlusWindow;
            this.timeOfLastStatusMessageNs = timeNs;
            this.receiverId = receiverId;
            this.address = receiverAddress;
        }
    }
}


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

import org.agrona.concurrent.UnsafeBuffer;

import static io.aeron.logbuffer.FrameDescriptor.*;
import static io.aeron.logbuffer.LogBufferDescriptor.applyDefaultHeader;
import static io.aeron.logbuffer.TermUnblocker.Status.*;
import static io.aeron.protocol.HeaderFlyweight.HDR_TYPE_PAD;

/**
 * Unblocks a term buffer if a publisher has died leaving the log with a partial log entry.
 */
public class TermUnblocker
{
    public enum Status
    {
        /**
         * No action has been taken during operation.
         */
        NO_ACTION,

        /**
         * The term has been unblocked so that the log can progress.
         */
        UNBLOCKED,

        /**
         * The term has been unblocked from the offset until the end of the term.
         */
        UNBLOCKED_TO_END,
    }

    /**
     * Attempt to unblock the current term at the current offset.
     *

     *
Current position length is > 0, then return

     *
Current position length is 0, scan forward by frame alignment until, one of the following:
     *

     *
reach a non-0 length, unblock up to indicated position (check original frame length for non-0)

     *
reach end of term and tail position >= end of term, unblock up to end of term (check original
     * frame length for non-0)
     *

     *
reach tail position < end of term, do NOT unblock

     *

     *

     *

     *
     * @param logMetaDataBuffer containing the default headers
     * @param termBuffer        to unblock
     * @param blockedOffset     to unblock at
     * @param tailOffset        to unblock up to
     * @param termId            for the current term.
     * @return whether unblocking was done, not done, or applied to end of term
     */
    public static Status unblock(
        final UnsafeBuffer logMetaDataBuffer,
        final UnsafeBuffer termBuffer,
        final int blockedOffset,
        final int tailOffset,
        final int termId)
    {
        Status status = NO_ACTION;
        int frameLength = frameLengthVolatile(termBuffer, blockedOffset);

        if (frameLength < 0)
        {
            resetHeader(logMetaDataBuffer, termBuffer, blockedOffset, termId, -frameLength);
            status = UNBLOCKED;
        }
        else if (0 == frameLength)
        {
            int currentOffset = blockedOffset + FRAME_ALIGNMENT;

            while (currentOffset < tailOffset)
            {
                frameLength = frameLengthVolatile(termBuffer, currentOffset);

                if (frameLength != 0)
                {
                    if (scanBackToConfirmZeroed(termBuffer, currentOffset, blockedOffset))
                    {
                        final int length = currentOffset - blockedOffset;
                        resetHeader(logMetaDataBuffer, termBuffer, blockedOffset, termId, length);
                        status = UNBLOCKED;
                    }

                    break;
                }

                currentOffset += FRAME_ALIGNMENT;
            }

            if (currentOffset == termBuffer.capacity())
            {
                if (0 == frameLengthVolatile(termBuffer, blockedOffset))
                {
                    final int length = currentOffset - blockedOffset;
                    resetHeader(logMetaDataBuffer, termBuffer, blockedOffset, termId, length);
                    status = UNBLOCKED_TO_END;
                }
            }
        }

        return status;
    }

    private static void resetHeader(
        final UnsafeBuffer logMetaDataBuffer,
        final UnsafeBuffer termBuffer,
        final int termOffset,
        final int termId,
        final int frameLength)
    {
        applyDefaultHeader(logMetaDataBuffer, termBuffer, termOffset);
        frameType(termBuffer, termOffset, HDR_TYPE_PAD);
        frameTermOffset(termBuffer, termOffset);
        frameTermId(termBuffer, termOffset, termId);
        frameLengthOrdered(termBuffer, termOffset, frameLength);
    }

    private static boolean scanBackToConfirmZeroed(final UnsafeBuffer buffer, final int from, final int limit)
    {
        int i = from - FRAME_ALIGNMENT;
        boolean allZeros = true;
        while (i >= limit)
        {
            if (0 != frameLengthVolatile(buffer, i))
            {
                allZeros = false;
                break;
            }

            i -= FRAME_ALIGNMENT;
        }

        return allZeros;
    }
}


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

import io.aeron.exceptions.AeronException;
import io.aeron.exceptions.DriverTimeoutException;
import org.agrona.DirectBuffer;
import org.agrona.IoUtil;
import org.agrona.SystemUtil;
import org.agrona.concurrent.AtomicBuffer;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.errors.ErrorConsumer;
import org.agrona.concurrent.errors.ErrorLogReader;
import org.agrona.concurrent.ringbuffer.ManyToOneRingBuffer;

import java.io.File;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import java.util.function.Consumer;

import static io.aeron.Aeron.sleep;
import static io.aeron.CncFileDescriptor.*;
import static java.lang.Long.getLong;
import static java.lang.System.getProperty;

/**
 * This class provides the Media Driver and client with common configuration for the Aeron directory.
 *

 * This class should have {@link #conclude()} called before the methods are used or at least
 * {@link #concludeAeronDirectory()} to avoid NPEs.
 *

 * Properties
 *

 *
aeron.dir: Use value as directory name for Aeron buffers and status.

 *

 */
public class CommonContext implements Cloneable
{
    /**
     * Property name for driver timeout after which the driver is considered inactive.
     */
    public static final String DRIVER_TIMEOUT_PROP_NAME = "aeron.driver.timeout";

    /**
     * Default timeout in which the driver is expected to respond or heartbeat.
     */
    public static final long DEFAULT_DRIVER_TIMEOUT_MS = 10_000;

    /**
     * Timeout in which the driver is expected to respond or heartbeat.
     */
    public static final long DRIVER_TIMEOUT_MS = getLong(DRIVER_TIMEOUT_PROP_NAME, DEFAULT_DRIVER_TIMEOUT_MS);

    /**
     * Value to represent a sessionId that is not to be used.
     */
    public static final int NULL_SESSION_ID = Aeron.NULL_VALUE;

    /**
     * The top level Aeron directory used for communication between a Media Driver and client.
     */
    public static final String AERON_DIR_PROP_NAME = "aeron.dir";

    /**
     * The value of the top level Aeron directory unless overridden by {@link #aeronDirectoryName(String)}
     */
    public static final String AERON_DIR_PROP_DEFAULT;

    /**
     * Media type used for IPC shared memory from {@link Publication} to {@link Subscription} channels.
     */
    public static final String IPC_MEDIA = "ipc";

    /**
     * Media type used for UDP sockets from {@link Publication} to {@link Subscription} channels.
     */
    public static final String UDP_MEDIA = "udp";

    /**
     * URI used for IPC {@link Publication}s and {@link Subscription}s
     */
    public static final String IPC_CHANNEL = "aeron:ipc";

    /**
     * URI used for Spy {@link Subscription}s whereby an outgoing unicast or multicast publication can be spied on
     * by IPC without receiving it again via the network.
     */
    public static final String SPY_PREFIX = "aeron-spy:";

    /**
     * The address and port used for a UDP channel. For the publisher it is the socket to send to,
     * for the subscriber it is the socket to receive from.
     */
    public static final String ENDPOINT_PARAM_NAME = "endpoint";

    /**
     * The network interface via which the socket will be routed.
     */
    public static final String INTERFACE_PARAM_NAME = "interface";

    /**
     * Initial term id to be used when creating an {@link ExclusivePublication}.
     */
    public static final String INITIAL_TERM_ID_PARAM_NAME = "init-term-id";

    /**
     * Current term id to be used when creating an {@link ExclusivePublication}.
     */
    public static final String TERM_ID_PARAM_NAME = "term-id";

    /**
     * Current term offset to be used when creating an {@link ExclusivePublication}.
     */
    public static final String TERM_OFFSET_PARAM_NAME = "term-offset";

    /**
     * The param name to be used for the term length as a channel URI param.
     */
    public static final String TERM_LENGTH_PARAM_NAME = "term-length";

    /**
     * MTU length parameter name for using as a channel URI param. If this is greater than the network MTU for UDP
     * then the packet will be fragmented and can amplify the impact of loss.
     */
    public static final String MTU_LENGTH_PARAM_NAME = "mtu";

    /**
     * Time To Live param for a multicast datagram.
     */
    public static final String TTL_PARAM_NAME = "ttl";

    /**
     * The param for the control channel IP address and port for multi-destination-cast semantics.
     */
    public static final String MDC_CONTROL_PARAM_NAME = "control";

    /**
     * Key for the mode of control that such be used for multi-destination-cast semantics.
     */
    public static final String MDC_CONTROL_MODE_PARAM_NAME = "control-mode";

    /**
     * Valid value for {@link #MDC_CONTROL_MODE_PARAM_NAME} when manual control is desired.
     */
    public static final String MDC_CONTROL_MODE_MANUAL = "manual";

    /**
     * Valid value for {@link #MDC_CONTROL_MODE_PARAM_NAME} when dynamic control is desired. Default value.
     */
    public static final String MDC_CONTROL_MODE_DYNAMIC = "dynamic";

    /**
     * Key for the session id for a publication or restricted subscription.
     */
    public static final String SESSION_ID_PARAM_NAME = "session-id";

    /**
     * Key for the linger timeout for a publication to wait around after draining in nanoseconds.
     */
    public static final String LINGER_PARAM_NAME = "linger";

    /**
     * Parameter name for channel URI param to indicate if a subscribed stream must be reliable or not.
     * Value is boolean with true to recover loss and false to gap fill.
     */
    public static final String RELIABLE_STREAM_PARAM_NAME = "reliable";

    /**
     * Key for the tags for a channel
     */
    public static final String TAGS_PARAM_NAME = "tags";

    /**
     * Qualifier for a value which is a tag for reference. This prefix is use in the param value.
     */
    public static final String TAG_PREFIX = "tag:";

    /**
     * Parameter name for channel URI param to indicate if term buffers should be sparse. Value is boolean.
     */
    public static final String SPARSE_PARAM_NAME = "sparse";

    /**
     * Parameter name for channel URI param to indicate an alias for the given URI. Value not interpreted by Aeron.
     */
    public static final String ALIAS_PARAM_NAME = "alias";

    /**
     * Parameter name for channel URI param to indicate if End of Stream (EOS) should be sent or not. Value is boolean.
     */
    public static final String EOS_PARAM_NAME = "eos";

    /**
     * Parameter name for channel URI param to indicate if a subscription should tether for local flow control.
     * Value is boolean. A tether only applies when there is more than one matching active subscription. If tether is
     * true then that subscription is included in flow control. If only one subscription then it tethers pace.
     */
    public static final String TETHER_PARAM_NAME = "tether";

    private long driverTimeoutMs = DRIVER_TIMEOUT_MS;
    private String aeronDirectoryName = getAeronDirectoryName();
    private File aeronDirectory;
    private File cncFile;
    private UnsafeBuffer countersMetaDataBuffer;
    private UnsafeBuffer countersValuesBuffer;

    static
    {
        String baseDirName = null;

        if (SystemUtil.osName().contains("linux"))
        {
            final File devShmDir = new File("/dev/shm");

            if (devShmDir.exists())
            {
                baseDirName = "/dev/shm/aeron";
            }
        }

        if (null == baseDirName)
        {
            baseDirName = SystemUtil.tmpDirName() + "aeron";
        }

        AERON_DIR_PROP_DEFAULT = baseDirName + '-' + System.getProperty("user.name", "default");
    }

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

    /**
     * Get the default directory name to be used if {@link #aeronDirectoryName(String)} is not set. This will take
     * the {@link #AERON_DIR_PROP_NAME} if set and if not then {@link #AERON_DIR_PROP_DEFAULT}.
     *
     * @return the default directory name to be used if {@link #aeronDirectoryName(String)} is not set.
     */
    public static String getAeronDirectoryName()
    {
        return getProperty(AERON_DIR_PROP_NAME, AERON_DIR_PROP_DEFAULT);
    }

    /**
     * Convert the default Aeron directory name to be a random name for use with embedded drivers.
     *
     * @return random directory name with default directory name as base
     */
    public static String generateRandomDirName()
    {
        return AERON_DIR_PROP_DEFAULT + '-' + UUID.randomUUID().toString();
    }

    /**
     * This completes initialization of the CommonContext object. It is automatically called by subclasses.
     *
     * @return this Object for method chaining.
     */
    public CommonContext conclude()
    {
        concludeAeronDirectory();

        cncFile = new File(aeronDirectory, CncFileDescriptor.CNC_FILE);

        return this;
    }

    /**
     * Conclude the {@link #aeronDirectory()} so it does not need to keep being recreated.
     *
     * @return this for a fluent API.
     */
    public CommonContext concludeAeronDirectory()
    {
        if (null == aeronDirectory)
        {
            aeronDirectory = new File(aeronDirectoryName);
        }

        return this;
    }

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

    /**
     * Get the directory in which the aeron config files are stored.
     *

     * This is valid after a call to {@link #conclude()}.
     *
     * @return the directory in which the aeron config files are stored.
     * @see #aeronDirectoryName()
     */
    public File aeronDirectory()
    {
        return aeronDirectory;
    }

    /**
     * Set the top level Aeron directory used for communication between the client and Media Driver, and the location
     * of the data buffers.
     *
     * @param dirName New top level Aeron directory.
     * @return this for a fluent API.
     */
    public CommonContext aeronDirectoryName(final String dirName)
    {
        this.aeronDirectoryName = dirName;
        return this;
    }

    /**
     * Create a new command and control file in the administration directory.
     *
     * @return The newly created File.
     */
    public static File newDefaultCncFile()
    {
        return new File(getProperty(AERON_DIR_PROP_NAME, AERON_DIR_PROP_DEFAULT), CncFileDescriptor.CNC_FILE);
    }

    /**
     * Get the buffer containing the counter meta data. These counters are R/W for the driver, read only for all
     * other users.
     *
     * @return The buffer storing the counter meta data.
     */
    public UnsafeBuffer countersMetaDataBuffer()
    {
        return countersMetaDataBuffer;
    }

    /**
     * Set the buffer containing the counter meta data. Testing/internal purposes only.
     *
     * @param countersMetaDataBuffer The new counter meta data buffer.
     * @return this for a fluent API.
     */
    public CommonContext countersMetaDataBuffer(final UnsafeBuffer countersMetaDataBuffer)
    {
        this.countersMetaDataBuffer = countersMetaDataBuffer;
        return this;
    }

    /**
     * Get the buffer containing the counters. These counters are R/W for the driver, read only for all other users.
     *
     * @return The buffer storing the counters.
     */
    public UnsafeBuffer countersValuesBuffer()
    {
        return countersValuesBuffer;
    }

    /**
     * Set the buffer containing the counters. Testing/internal purposes only.
     *
     * @param countersValuesBuffer The new counters buffer.
     * @return this for a fluent API.
     */
    public CommonContext countersValuesBuffer(final UnsafeBuffer countersValuesBuffer)
    {
        this.countersValuesBuffer = countersValuesBuffer;
        return this;
    }

    /**
     * Get the command and control file.
     *
     * @return The command and control file.
     */
    public File cncFile()
    {
        return cncFile;
    }

    /**
     * Set the driver timeout in milliseconds
     *
     * @param driverTimeoutMs to indicate liveness of driver
     * @return this for a fluent API.
     */
    public CommonContext driverTimeoutMs(final long driverTimeoutMs)
    {
        this.driverTimeoutMs = driverTimeoutMs;
        return this;
    }

    /**
     * Get the driver timeout in milliseconds.
     *
     * @return driver timeout in milliseconds.
     */
    public long driverTimeoutMs()
    {
        return driverTimeoutMs;
    }

    /**
     * Delete the current Aeron directory, throwing errors if not possible.
     */
    public void deleteAeronDirectory()
    {
        IoUtil.delete(aeronDirectory, false);
    }

    /**
     * Map the CnC file if it exists.
     *
     * @param logger for feedback
     * @return a new mapping for the file if it exists otherwise null;
     */
    public MappedByteBuffer mapExistingCncFile(final Consumer<String> logger)
    {
        final File cncFile = new File(aeronDirectory, CncFileDescriptor.CNC_FILE);

        if (cncFile.exists() && cncFile.length() > 0)
        {
            if (null != logger)
            {
                logger.accept("INFO: Aeron CnC file exists: " + cncFile);
            }

            return IoUtil.mapExistingFile(cncFile, CncFileDescriptor.CNC_FILE);
        }

        return null;
    }

    /**
     * Is a media driver active in the given directory?
     *
     * @param directory       to check
     * @param driverTimeoutMs for the driver liveness check.
     * @param logger          for feedback as liveness checked.
     * @return true if a driver is active or false if not.
     */
    public static boolean isDriverActive(
        final File directory, final long driverTimeoutMs, final Consumer<String> logger)
    {
        final File cncFile = new File(directory, CncFileDescriptor.CNC_FILE);

        if (cncFile.exists() && cncFile.length() > 0)
        {
            logger.accept("INFO: Aeron CnC file exists: " + cncFile);

            final MappedByteBuffer cncByteBuffer = IoUtil.mapExistingFile(cncFile, "CnC file");
            try
            {
                return isDriverActive(driverTimeoutMs, logger, cncByteBuffer);
            }
            finally
            {
                IoUtil.unmap(cncByteBuffer);
            }
        }

        return false;
    }

    /**
     * Is a media driver active in the current Aeron directory?
     *
     * @param driverTimeoutMs for the driver liveness check.
     * @param logger          for feedback as liveness checked.
     * @return true if a driver is active or false if not.
     */
    public boolean isDriverActive(final long driverTimeoutMs, final Consumer<String> logger)
    {
        final MappedByteBuffer cncByteBuffer = mapExistingCncFile(logger);
        try
        {
            return isDriverActive(driverTimeoutMs, logger, cncByteBuffer);
        }
        finally
        {
            IoUtil.unmap(cncByteBuffer);
        }
    }

    /**
     * Is a media driver active in the current mapped CnC buffer? If the driver is mid start then it will wait for
     * up to the driverTimeoutMs by checking for the cncVersion being set.
     *
     * @param driverTimeoutMs for the driver liveness check.
     * @param logger          for feedback as liveness checked.
     * @param cncByteBuffer   for the existing CnC file.
     * @return true if a driver is active or false if not.
     */
    public static boolean isDriverActive(
        final long driverTimeoutMs, final Consumer<String> logger, final ByteBuffer cncByteBuffer)
    {
        if (null == cncByteBuffer)
        {
            return false;
        }

        final UnsafeBuffer cncMetaDataBuffer = CncFileDescriptor.createMetaDataBuffer(cncByteBuffer);

        final long startTimeMs = System.currentTimeMillis();
        int cncVersion;
        while (0 == (cncVersion = cncMetaDataBuffer.getIntVolatile(CncFileDescriptor.cncVersionOffset(0))))
        {
            if (System.currentTimeMillis() > (startTimeMs + driverTimeoutMs))
            {
                throw new DriverTimeoutException("CnC file is created but not initialised.");
            }

            sleep(1);
        }

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

        final ManyToOneRingBuffer toDriverBuffer = new ManyToOneRingBuffer(
            CncFileDescriptor.createToDriverBuffer(cncByteBuffer, cncMetaDataBuffer));

        final long timestamp = toDriverBuffer.consumerHeartbeatTime();
        final long now = System.currentTimeMillis();
        final long timestampAge = now - timestamp;

        logger.accept("INFO: Aeron toDriver consumer heartbeat is (ms): " + timestampAge);

        return timestampAge <= driverTimeoutMs;
    }

    /**
     * Request a driver to run its termination hook.
     *
     * @param directory for the driver.
     * @param tokenBuffer containing the optional token for the request.
     * @param tokenOffset within the tokenBuffer at which the token begins.
     * @param tokenLength of the token in the tokenBuffer.
     * @return true if request was sent or false if request could not be sent.
     */
    public static boolean requestDriverTermination(
        final File directory,
        final DirectBuffer tokenBuffer,
        final int tokenOffset,
        final int tokenLength)
    {
        final File cncFile = new File(directory, CncFileDescriptor.CNC_FILE);

        if (cncFile.exists() && cncFile.length() > 0)
        {
            final MappedByteBuffer cncByteBuffer = IoUtil.mapExistingFile(cncFile, "CnC file");
            try
            {
                final UnsafeBuffer cncMetaDataBuffer = CncFileDescriptor.createMetaDataBuffer(cncByteBuffer);
                final int cncVersion = cncMetaDataBuffer.getIntVolatile(cncVersionOffset(0));

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

                final ManyToOneRingBuffer toDriverBuffer = new ManyToOneRingBuffer(
                    CncFileDescriptor.createToDriverBuffer(cncByteBuffer, cncMetaDataBuffer));
                final long clientId = toDriverBuffer.nextCorrelationId();

                final DriverProxy driverProxy = new DriverProxy(toDriverBuffer, clientId);

                return driverProxy.terminateDriver(tokenBuffer, tokenOffset, tokenLength);
            }
            finally
            {
                IoUtil.unmap(cncByteBuffer);
            }
        }

        return false;
    }

    /**
     * Read the error log to a given {@link PrintStream}
     *
     * @param out to write the error log contents to.
     * @return the number of observations from the error log.
     */
    public int saveErrorLog(final PrintStream out)
    {
        final MappedByteBuffer cncByteBuffer = mapExistingCncFile(null);
        try
        {
            return saveErrorLog(out, cncByteBuffer);
        }
        finally
        {
            IoUtil.unmap(cncByteBuffer);
        }
    }

    /**
     * Read the error log to a given {@link PrintStream}
     *
     * @param out           to write the error log contents to.
     * @param cncByteBuffer containing the error log.
     * @return the number of observations from the error log.
     */
    public int saveErrorLog(final PrintStream out, final ByteBuffer cncByteBuffer)
    {
        if (null == cncByteBuffer)
        {
            return 0;
        }

        final UnsafeBuffer cncMetaDataBuffer = CncFileDescriptor.createMetaDataBuffer(cncByteBuffer);
        final int cncVersion = cncMetaDataBuffer.getInt(CncFileDescriptor.cncVersionOffset(0));

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

        int distinctErrorCount = 0;
        final AtomicBuffer buffer = CncFileDescriptor.createErrorLogBuffer(cncByteBuffer, cncMetaDataBuffer);
        if (ErrorLogReader.hasErrors(buffer))
        {
            final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ");
            final ErrorConsumer errorConsumer = (count, firstTimestamp, lastTimestamp, ex) ->
                formatError(out, dateFormat, count, firstTimestamp, lastTimestamp, ex);

            distinctErrorCount = ErrorLogReader.read(buffer, errorConsumer);
        }

        out.println();
        out.println(distinctErrorCount + " distinct errors observed.");

        return distinctErrorCount;
    }

    /**
     * Release resources used by the CommonContext.
     */
    public void close()
    {
    }

    public static void formatError(
        final PrintStream out,
        final SimpleDateFormat dateFormat,
        final int observationCount,
        final long firstObservationTimestamp,
        final long lastObservationTimestamp,
        final String encodedException)
    {
        out.format(
            "***%n%d observations from %s to %s for:%n %s%n",
            observationCount,
            dateFormat.format(new Date(firstObservationTimestamp)),
            dateFormat.format(new Date(lastObservationTimestamp)),
            encodedException);
    }
}


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

import io.aeron.protocol.*;
import org.agrona.concurrent.UnsafeBuffer;

import java.nio.ByteOrder;

import static java.nio.ByteOrder.LITTLE_ENDIAN;

/**
 * Description of the structure for message framing in a log buffer.
 *

 * All messages are logged in frames that have a minimum header layout as follows plus a reserve then
 * the encoded message follows:
 *

 *   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
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |R|                       Frame Length                          |
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------------------------+
 *  |  Version      |B|E| Flags     |             Type              |
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------------------------+
 *  |R|                       Term Offset                           |
 *  +-+-------------------------------------------------------------+
 *  |                      Additional Fields                       ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *  |                        Encoded Message                       ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *

 *

 * The (B)egin and (E)nd flags are used for message fragmentation. R is for reserved bit.
 * Both (B)egin and (E)nd flags are set for a message that does not span frames.
 */
public class FrameDescriptor
{
    /**
     * Set a pragmatic maximum message length regardless of term length to encourage better design.
     * Messages larger than half the cache size should be broken up into chunks and streamed.
     */
    public static final int MAX_MESSAGE_LENGTH = 16 * 1024 * 1024;

    /**
     * Alignment as a multiple of bytes for each frame. The length field will store the unaligned length in bytes.
     */
    public static final int FRAME_ALIGNMENT = 32;

    /**
     * Beginning fragment of a frame.
     */
    public static final byte BEGIN_FRAG_FLAG = (byte)0b1000_0000;

    /**
     * End fragment of a frame.
     */
    public static final byte END_FRAG_FLAG = (byte)0b0100_0000;

    /**
     * End fragment of a frame.
     */
    public static final byte UNFRAGMENTED = BEGIN_FRAG_FLAG | END_FRAG_FLAG;

    /**
     * Offset within a frame at which the version field begins
     */
    public static final int VERSION_OFFSET = DataHeaderFlyweight.VERSION_FIELD_OFFSET;

    /**
     * Offset within a frame at which the flags field begins
     */
    public static final int FLAGS_OFFSET = DataHeaderFlyweight.FLAGS_FIELD_OFFSET;

    /**
     * Offset within a frame at which the type field begins
     */
    public static final int TYPE_OFFSET = DataHeaderFlyweight.TYPE_FIELD_OFFSET;

    /**
     * Offset within a frame at which the term offset field begins
     */
    public static final int TERM_OFFSET = DataHeaderFlyweight.TERM_OFFSET_FIELD_OFFSET;

    /**
     * Offset within a frame at which the term id field begins
     */
    public static final int TERM_ID_OFFSET = DataHeaderFlyweight.TERM_ID_FIELD_OFFSET;

    /**
     * Padding frame type to indicate the message should be ignored.
     */
    public static final int PADDING_FRAME_TYPE = HeaderFlyweight.HDR_TYPE_PAD;

    /**
     * Compute the maximum supported message length for a buffer of given termLength.
     *
     * @param termLength of the log buffer.
     * @return the maximum supported length for a message.
     */
    public static int computeMaxMessageLength(final int termLength)
    {
        return Math.min(termLength / 8, MAX_MESSAGE_LENGTH);
    }

    /**
     * The buffer offset at which the length field begins.
     *
     * @param termOffset at which the frame begins.
     * @return the offset at which the length field begins.
     */
    public static int lengthOffset(final int termOffset)
    {
        return termOffset;
    }

    /**
     * The buffer offset at which the version field begins.
     *
     * @param termOffset at which the frame begins.
     * @return the offset at which the version field begins.
     */
    public static int versionOffset(final int termOffset)
    {
        return termOffset + VERSION_OFFSET;
    }

    /**
     * The buffer offset at which the flags field begins.
     *
     * @param termOffset at which the frame begins.
     * @return the offset at which the flags field begins.
     */
    public static int flagsOffset(final int termOffset)
    {
        return termOffset + FLAGS_OFFSET;
    }

    /**
     * The buffer offset at which the type field begins.
     *
     * @param termOffset at which the frame begins.
     * @return the offset at which the type field begins.
     */
    public static int typeOffset(final int termOffset)
    {
        return termOffset + TYPE_OFFSET;
    }

    /**
     * The buffer offset at which the term offset field begins.
     *
     * @param termOffset at which the frame begins.
     * @return the offset at which the term offset field begins.
     */
    public static int termOffsetOffset(final int termOffset)
    {
        return termOffset + TERM_OFFSET;
    }

    /**
     * The buffer offset at which the term id field begins.
     *
     * @param termOffset at which the frame begins.
     * @return the offset at which the term id field begins.
     */
    public static int termIdOffset(final int termOffset)
    {
        return termOffset + TERM_ID_OFFSET;
    }

    /**
     * Read the type of of the frame from header.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @return the value of the frame type header.
     */
    public static int frameVersion(final UnsafeBuffer buffer, final int termOffset)
    {
        return buffer.getByte(versionOffset(termOffset));
    }

    /**
     * Get the flags field for a frame.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @return the value of the flags.
     */
    public static byte frameFlags(final UnsafeBuffer buffer, final int termOffset)
    {
        return buffer.getByte(flagsOffset(termOffset));
    }

    /**
     * Read the type of of the frame from header.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @return the value of the frame type header.
     */
    public static int frameType(final UnsafeBuffer buffer, final int termOffset)
    {
        return buffer.getShort(typeOffset(termOffset), LITTLE_ENDIAN) & 0xFFFF;
    }

    /**
     * Is the frame starting at the termOffset a padding frame at the end of a buffer?
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @return true if the frame is a padding frame otherwise false.
     */
    public static boolean isPaddingFrame(final UnsafeBuffer buffer, final int termOffset)
    {
        return buffer.getShort(typeOffset(termOffset)) == PADDING_FRAME_TYPE;
    }

    /**
     * Get the length of a frame from the header.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @return the value for the frame length.
     */
    public static int frameLength(final UnsafeBuffer buffer, final int termOffset)
    {
        return buffer.getInt(termOffset, LITTLE_ENDIAN);
    }

    /**
     * Get the length of a frame from the header as a volatile read.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @return the value for the frame length.
     */
    public static int frameLengthVolatile(final UnsafeBuffer buffer, final int termOffset)
    {
        int frameLength = buffer.getIntVolatile(termOffset);

        if (ByteOrder.nativeOrder() != LITTLE_ENDIAN)
        {
            frameLength = Integer.reverseBytes(frameLength);
        }

        return frameLength;
    }

    /**
     * Write the length header for a frame in a memory ordered fashion.
     *
     * @param buffer      containing the frame.
     * @param termOffset  at which a frame begins.
     * @param frameLength field to be set for the frame.
     */
    public static void frameLengthOrdered(final UnsafeBuffer buffer, final int termOffset, final int frameLength)
    {
        int length = frameLength;
        if (ByteOrder.nativeOrder() != LITTLE_ENDIAN)
        {
            length = Integer.reverseBytes(frameLength);
        }

        buffer.putIntOrdered(termOffset, length);
    }

    /**
     * Write the type field for a frame.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @param type       type value for the frame.
     */
    public static void frameType(final UnsafeBuffer buffer, final int termOffset, final int type)
    {
        buffer.putShort(typeOffset(termOffset), (short)type, LITTLE_ENDIAN);
    }

    /**
     * Write the flags field for a frame.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @param flags      value for the frame.
     */
    public static void frameFlags(final UnsafeBuffer buffer, final int termOffset, final byte flags)
    {
        buffer.putByte(flagsOffset(termOffset), flags);
    }

    /**
     * Write the term offset field for a frame.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     */
    public static void frameTermOffset(final UnsafeBuffer buffer, final int termOffset)
    {
        buffer.putInt(termOffsetOffset(termOffset), termOffset, LITTLE_ENDIAN);
    }

    /**
     * Write the term id field for a frame.
     *
     * @param buffer     containing the frame.
     * @param termOffset at which a frame begins.
     * @param termId     value for the frame.
     */
    public static void frameTermId(final UnsafeBuffer buffer, final int termOffset, final int termId)
    {
        buffer.putInt(termIdOffset(termOffset), termId, LITTLE_ENDIAN);
    }
}


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

import io.aeron.CommonContext;
import io.aeron.driver.*;
import io.aeron.status.ChannelEndpointStatus;
import io.aeron.protocol.NakFlyweight;
import io.aeron.protocol.RttMeasurementFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import org.agrona.collections.BiInt2ObjectMap;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.status.AtomicCounter;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.PortUnreachableException;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

import static io.aeron.status.ChannelEndpointStatus.status;
import static io.aeron.driver.status.SystemCounterDescriptor.*;
import static io.aeron.protocol.StatusMessageFlyweight.SEND_SETUP_FLAG;

/**
 * Aggregator of multiple {@link NetworkPublication}s onto a single transport channel for
 * sending data and setup frames plus the receiving of status and NAK frames.
 */
public class SendChannelEndpoint extends UdpChannelTransport
{
    private static final long DESTINATION_TIMEOUT = TimeUnit.SECONDS.toNanos(5);

    private int refCount = 0;
    private final BiInt2ObjectMap<NetworkPublication> publicationBySessionAndStreamId = new BiInt2ObjectMap<>();
    private final MultiDestination multiDestination;
    private final AtomicCounter statusMessagesReceived;
    private final AtomicCounter nakMessagesReceived;
    private final AtomicCounter statusIndicator;

    public SendChannelEndpoint(
        final UdpChannel udpChannel, final AtomicCounter statusIndicator, final MediaDriver.Context context)
    {
        super(
            udpChannel,
            udpChannel.remoteControl(),
            udpChannel.localControl(),
            !udpChannel.hasExplicitControl() ? udpChannel.remoteData() : null,
            context);

        nakMessagesReceived = context.systemCounters().get(NAK_MESSAGES_RECEIVED);
        statusMessagesReceived = context.systemCounters().get(STATUS_MESSAGES_RECEIVED);
        this.statusIndicator = statusIndicator;

        MultiDestination multiDestination = null;
        if (udpChannel.hasExplicitControl())
        {
            final String mode = udpChannel.channelUri().get(CommonContext.MDC_CONTROL_MODE_PARAM_NAME);
            if (CommonContext.MDC_CONTROL_MODE_MANUAL.equals(mode))
            {
                multiDestination = new ManualMultiDestination();
            }
            else if (null == mode || CommonContext.MDC_CONTROL_MODE_DYNAMIC.equals(mode))
            {
                multiDestination = new DynamicMultiDestination(context.cachedNanoClock(), DESTINATION_TIMEOUT);
            }
        }

        this.multiDestination = multiDestination;
    }

    public void decRef()
    {
        --refCount;
    }

    public void incRef()
    {
        ++refCount;
    }

    public void openChannel(final DriverConductorProxy conductorProxy)
    {
        if (conductorProxy.notConcurrent())
        {
            openDatagramChannel(statusIndicator);
        }
        else
        {
            try
            {
                openDatagramChannel(statusIndicator);
            }
            catch (final Exception ex)
            {
                conductorProxy.channelEndpointError(statusIndicator.id(), ex);
                throw ex;
            }
        }
    }

    public String originalUriString()
    {
        return udpChannel().originalUriString();
    }

    public int statusIndicatorCounterId()
    {
        return statusIndicator.id();
    }

    public void indicateActive()
    {
        final long currentStatus = statusIndicator.get();
        if (currentStatus != ChannelEndpointStatus.INITIALIZING)
        {
            throw new IllegalStateException(
                "channel cannot be registered unless INITALIZING: status=" + status(currentStatus));
        }

        statusIndicator.setOrdered(ChannelEndpointStatus.ACTIVE);
    }

    public void closeStatusIndicator()
    {
        if (!statusIndicator.isClosed())
        {
            statusIndicator.setOrdered(ChannelEndpointStatus.CLOSING);
            statusIndicator.close();
        }
    }

    /**
     * Called by the {@link DriverConductor} to determine if the channel endpoint should be closed.
     *
     * @return true if ready to be closed.
     */
    public boolean shouldBeClosed()
    {
        return 0 == refCount && !statusIndicator.isClosed();
    }

    /**
     * Called from the {@link Sender} to add information to the control packet dispatcher.
     *
     * @param publication to add to the dispatcher
     */
    public void registerForSend(final NetworkPublication publication)
    {
        publicationBySessionAndStreamId.put(publication.sessionId(), publication.streamId(), publication);
    }

    /**
     * Called from the {@link Sender} to remove information from the control packet dispatcher.
     *
     * @param publication to remove
     */
    public void unregisterForSend(final NetworkPublication publication)
    {
        publicationBySessionAndStreamId.remove(publication.sessionId(), publication.streamId());
    }

    /**
     * Send contents of a {@link ByteBuffer} to connected address.
     * This is used on the sender side for performance over send(ByteBuffer, SocketAddress).
     *
     * @param buffer to send
     * @return number of bytes sent
     */
    public int send(final ByteBuffer buffer)
    {
        int bytesSent = 0;

        if (null != sendDatagramChannel)
        {
            final int bytesToSend = buffer.remaining();

            if (null == multiDestination)
            {
                try
                {
                    sendHook(buffer, connectAddress);
                    if (sendDatagramChannel.isConnected())
                    {
                        bytesSent = sendDatagramChannel.write(buffer);
                    }
                }
                catch (final PortUnreachableException ignore)
                {
                }
                catch (final IOException ex)
                {
                    sendError(bytesToSend, ex, connectAddress);
                }
            }
            else
            {
                bytesSent = multiDestination.send(sendDatagramChannel, buffer, this, bytesToSend);
            }
        }

        return bytesSent;
    }

    public void onStatusMessage(
        final StatusMessageFlyweight msg,
        final UnsafeBuffer buffer,
        final int length,
        final InetSocketAddress srcAddress)
    {
        final int sessionId = msg.sessionId();
        final int streamId = msg.streamId();
        final NetworkPublication publication = publicationBySessionAndStreamId.get(sessionId, streamId);

        if (null != multiDestination)
        {
            multiDestination.onStatusMessage(msg, srcAddress);

            if (0 == sessionId && 0 == streamId && SEND_SETUP_FLAG == (msg.flags() & SEND_SETUP_FLAG))
            {
                publicationBySessionAndStreamId.forEach(NetworkPublication::triggerSendSetupFrame);
                statusMessagesReceived.incrementOrdered();
            }
        }

        if (null != publication)
        {
            if (SEND_SETUP_FLAG == (msg.flags() & SEND_SETUP_FLAG))
            {
                publication.triggerSendSetupFrame();
            }
            else
            {
                publication.onStatusMessage(msg, srcAddress);
            }

            statusMessagesReceived.incrementOrdered();
        }
    }

    public void onNakMessage(
        final NakFlyweight msg,
        final UnsafeBuffer buffer,
        final int length,
        final InetSocketAddress srcAddress)
    {
        final NetworkPublication publication = publicationBySessionAndStreamId.get(msg.sessionId(), msg.streamId());

        if (null != publication)
        {
            publication.onNak(msg.termId(), msg.termOffset(), msg.length());
            nakMessagesReceived.incrementOrdered();
        }
    }

    public void onRttMeasurement(
        final RttMeasurementFlyweight msg,
        final UnsafeBuffer buffer,
        final int length,
        final InetSocketAddress srcAddress)
    {
        final NetworkPublication publication = publicationBySessionAndStreamId.get(msg.sessionId(), msg.streamId());

        if (null != publication)
        {
            publication.onRttMeasurement(msg, srcAddress);
        }
    }

    public void validateAllowsManualControl()
    {
        if (null == multiDestination || !multiDestination.isManualControlMode())
        {
            throw new IllegalArgumentException("control channel does not allow manual control");
        }
    }

    public void addDestination(final InetSocketAddress address)
    {
        multiDestination.addDestination(address);
    }

    public void removeDestination(final InetSocketAddress address)
    {
        multiDestination.removeDestination(address);
    }
}