public static final class PaddedEPath extends EPath {

    public PaddedEPath(List<EPathSegment> segments) {
        this(segments.toArray(new EPathSegment[segments.size()]));
    }

    public PaddedEPath(EPathSegment... segments) {
        super(segments);
    }

    @Override
    public boolean isPadded() {
        return true;
    }

    public PaddedEPath append(PaddedEPath other) {
        int aLen = getSegments().length;
        int bLen = other.getSegments().length;

        EPathSegment[] newSegments = new EPathSegment[aLen + bLen];

        System.arraycopy(getSegments(), 0, newSegments, 0, aLen);
        System.arraycopy(other.getSegments(), 0, newSegments, aLen, bLen);

        return new PaddedEPath(newSegments);
    }

} public static final class ClassId extends LogicalSegment<Integer> {

    public ClassId(Integer value) {
        this(value, smallestFormat(value));
    }

    public ClassId(int value, LogicalFormat format) {
        super(value, LogicalType.ClassId, format);
    }

    @Override
    protected ByteBuf encodeValue(ByteBuf buffer) {
        return encodeIntValue(getFormat(), getValue(), buffer);
    }

} public static final class InstanceId extends LogicalSegment<Integer> {

    public InstanceId(int value) {
        this(value, smallestFormat(value));
    }

    public InstanceId(int value, LogicalFormat format) {
        super(value, LogicalType.InstanceId, format);
    }

    @Override
    protected ByteBuf encodeValue(ByteBuf buffer) {
        return encodeIntValue(getFormat(), getValue(), buffer);
    }

} public static final class PartialResponseException extends Exception {
    public static final PartialResponseException INSTANCE = new PartialResponseException();
} public class ForwardCloseService implements CipService<ForwardCloseResponse> {

    public static final int SERVICE_CODE = 0x4E;

    private static final PaddedEPath CONNECTION_MANAGER_PATH = new PaddedEPath(
        new ClassId(0x06),
        new InstanceId(0x01)
    );

    private final ForwardCloseRequest request;

    public ForwardCloseService(ForwardCloseRequest request) {
        this.request = request;
    }

    @Override
    public void encodeRequest(ByteBuf buffer) {
        MessageRouterRequest mrr = new MessageRouterRequest(
            SERVICE_CODE,
            CONNECTION_MANAGER_PATH,
            b -> ForwardCloseRequest.encode(request, b)
        );

        MessageRouterRequest.encode(mrr, buffer);
    }

    @Override
    public ForwardCloseResponse decodeResponse(ByteBuf buffer) throws CipResponseException, PartialResponseException {
        MessageRouterResponse mResponse = MessageRouterResponse.decode(buffer);

        int generalStatus = mResponse.getGeneralStatus();

        try {
            if (generalStatus == 0x00) {
                return ForwardCloseResponse.decode(mResponse.getData());
            } else {
                throw new CipResponseException(generalStatus, mResponse.getAdditionalStatus());
            }
        } finally {
            ReferenceCountUtil.release(mResponse.getData());
        }
    }

} public class ForwardOpenService implements CipService<ForwardOpenResponse> {

    public static final int SERVICE_CODE = 0x54;

    private static final PaddedEPath CONNECTION_MANAGER_PATH = new PaddedEPath(
        new ClassId(0x06),
        new InstanceId(0x01)
    );

    private final ForwardOpenRequest request;

    public ForwardOpenService(ForwardOpenRequest request) {
        this.request = request;
    }

    @Override
    public void encodeRequest(ByteBuf buffer) {
        MessageRouterRequest mrr = new MessageRouterRequest(
            SERVICE_CODE,
            CONNECTION_MANAGER_PATH,
            this::encode
        );

        MessageRouterRequest.encode(mrr, buffer);
    }

    @Override
    public ForwardOpenResponse decodeResponse(ByteBuf buffer) throws CipResponseException, PartialResponseException {
        MessageRouterResponse mResponse = MessageRouterResponse.decode(buffer);

        int generalStatus = mResponse.getGeneralStatus();

        try {
            if (generalStatus == 0x00) {
                return ForwardOpenResponse.decode(mResponse.getData());
            } else {
                throw new CipResponseException(generalStatus, mResponse.getAdditionalStatus());
            }
        } finally {
            ReferenceCountUtil.release(mResponse.getData());
        }
    }

    private void encode(ByteBuf buffer) {
        ForwardOpenRequest.encode(request, buffer);
    }

} public class LargeForwardOpenService implements CipService<LargeForwardOpenResponse> {

    public static final int SERVICE_CODE = 0x5B;

    private static final PaddedEPath CONNECTION_MANAGER_PATH = new PaddedEPath(
        new ClassId(0x06),
        new InstanceId(0x01)
    );

    private final LargeForwardOpenRequest request;

    public LargeForwardOpenService(LargeForwardOpenRequest request) {
        this.request = request;
    }

    @Override
    public void encodeRequest(ByteBuf buffer) {
        MessageRouterRequest mrr = new MessageRouterRequest(
            SERVICE_CODE,
            CONNECTION_MANAGER_PATH,
            this::encode
        );

        MessageRouterRequest.encode(mrr, buffer);
    }

    @Override
    public LargeForwardOpenResponse decodeResponse(ByteBuf buffer) throws CipResponseException, PartialResponseException {
        MessageRouterResponse mResponse = MessageRouterResponse.decode(buffer);

        int generalStatus = mResponse.getGeneralStatus();

        try {
            if (generalStatus == 0x00) {
                return LargeForwardOpenResponse.decode(mResponse.getData());
            } else {
                throw new CipResponseException(generalStatus, mResponse.getAdditionalStatus());
            }
        } finally {
            ReferenceCountUtil.release(mResponse.getData());
        }
    }

    private void encode(ByteBuf buffer) {
        LargeForwardOpenRequest.encode(request, buffer);
    }

} public class ForwardCloseRequest {

    private final Duration connectionTimeout;
    private final int connectionSerialNumber;
    private final int originatorVendorId;
    private final long originatorSerialNumber;
    private final PaddedEPath connectionPath;

    public ForwardCloseRequest(Duration connectionTimeout,
                               int connectionSerialNumber,
                               int originatorVendorId,
                               long originatorSerialNumber,
                               PaddedEPath connectionPath) {

        this.connectionTimeout = connectionTimeout;
        this.connectionSerialNumber = connectionSerialNumber;
        this.originatorVendorId = originatorVendorId;
        this.originatorSerialNumber = originatorSerialNumber;
        this.connectionPath = connectionPath;
    }

    public Duration getConnectionTimeout() {
        return connectionTimeout;
    }

    public int getConnectionSerialNumber() {
        return connectionSerialNumber;
    }

    public int getOriginatorVendorId() {
        return originatorVendorId;
    }

    public long getOriginatorSerialNumber() {
        return originatorSerialNumber;
    }

    public PaddedEPath getConnectionPath() {
        return connectionPath;
    }

    public static ByteBuf encode(ForwardCloseRequest request, ByteBuf buffer) {
        int priorityAndTimeoutBytes = TimeoutCalculator.calculateTimeoutBytes(request.getConnectionTimeout());
        buffer.writeByte(priorityAndTimeoutBytes >> 8 & 0xFF);
        buffer.writeByte(priorityAndTimeoutBytes & 0xFF);

        buffer.writeShort(request.getConnectionSerialNumber());

        buffer.writeShort(request.getOriginatorVendorId());
        buffer.writeInt((int) request.getOriginatorSerialNumber());

        encodeConnectionPath(request.getConnectionPath(), buffer);

        return buffer;
    }

    /**
     * Encode the connection path.
     * <p>
     * {@link PaddedEPath#encode(EPath, ByteBuf)} can't be used here because the {@link ForwardCloseRequest} has an
     * extra reserved byte after the connection path size for some reason.
     *
     * @param path   the {@link PaddedEPath} to encode.
     * @param buffer the {@link ByteBuf} to encode into.
     */
    private static void encodeConnectionPath(PaddedEPath path, ByteBuf buffer) {
        // length placeholder...
        int lengthStartIndex = buffer.writerIndex();
        buffer.writeByte(0);

        // reserved
        buffer.writeZero(1);

        // encode the path segments...
        int dataStartIndex = buffer.writerIndex();

        for (EPathSegment segment : path.getSegments()) {
            if (segment instanceof LogicalSegment) {
                LogicalSegment.encode((LogicalSegment) segment, path.isPadded(), buffer);
            } else if (segment instanceof PortSegment) {
                PortSegment.encode((PortSegment) segment, path.isPadded(), buffer);
            } else if (segment instanceof DataSegment) {
                DataSegment.encode((DataSegment) segment, path.isPadded(), buffer);
            } else {
                throw new RuntimeException("no encoder for " + segment.getClass().getSimpleName());
            }
        }

        // go back and update the length
        int bytesWritten = buffer.writerIndex() - dataStartIndex;
        int wordsWritten = bytesWritten / 2;
        buffer.markWriterIndex();
        buffer.writerIndex(lengthStartIndex);
        buffer.writeByte(wordsWritten);
        buffer.resetWriterIndex();
    }

} public class ForwardCloseResponse {

    private final int connectionSerialNumber;
    private final int originatorVendorId;
    private final long originatorSerialNumber;

    public ForwardCloseResponse(int connectionSerialNumber, int originatorVendorId, long originatorSerialNumber) {
        this.connectionSerialNumber = connectionSerialNumber;
        this.originatorVendorId = originatorVendorId;
        this.originatorSerialNumber = originatorSerialNumber;
    }

    public int getConnectionSerialNumber() {
        return connectionSerialNumber;
    }

    public int getOriginatorVendorId() {
        return originatorVendorId;
    }

    public long getOriginatorSerialNumber() {
        return originatorSerialNumber;
    }

    public static ForwardCloseResponse decode(ByteBuf buffer) {
        int connectionSerialNumber = buffer.readUnsignedShort();
        int originatorVendorId = buffer.readUnsignedShort();
        long originatorSerialNumber = buffer.readUnsignedInt();

        return new ForwardCloseResponse(connectionSerialNumber, originatorVendorId, originatorSerialNumber);
    }

} public final class ForwardOpenRequest {

    private final Duration timeout;
    private final int o2tConnectionId;
    private final int t2oConnectionId;
    private final int connectionSerialNumber;
    private final int vendorId;
    private final long vendorSerialNumber;
    private final int connectionTimeoutMultiplier;
    private final EPath.PaddedEPath connectionPath;
    private final Duration o2tRpi;
    private final NetworkConnectionParameters o2tParameters;
    private final Duration t2oRpi;
    private final NetworkConnectionParameters t2oParameters;
    private final int transportClassAndTrigger;

    public ForwardOpenRequest(Duration timeout,
                              int o2tConnectionId,
                              int t2oConnectionId,
                              int connectionSerialNumber,
                              int vendorId,
                              long vendorSerialNumber,
                              int connectionTimeoutMultiplier,
                              EPath.PaddedEPath connectionPath,
                              Duration o2tRpi,
                              NetworkConnectionParameters o2tParameters,
                              Duration t2oRpi,
                              NetworkConnectionParameters t2oParameters,
                              int transportClassAndTrigger) {

        this.timeout = timeout;
        this.o2tConnectionId = o2tConnectionId;
        this.t2oConnectionId = t2oConnectionId;
        this.connectionSerialNumber = connectionSerialNumber;
        this.vendorId = vendorId;
        this.vendorSerialNumber = vendorSerialNumber;
        this.connectionTimeoutMultiplier = connectionTimeoutMultiplier;
        this.connectionPath = connectionPath;
        this.o2tRpi = o2tRpi;
        this.o2tParameters = o2tParameters;
        this.t2oRpi = t2oRpi;
        this.t2oParameters = t2oParameters;
        this.transportClassAndTrigger = transportClassAndTrigger;
    }

    public Duration getTimeout() {
        return timeout;
    }

    public int getO2tConnectionId() {
        return o2tConnectionId;
    }

    public int getT2oConnectionId() {
        return t2oConnectionId;
    }

    public int getConnectionSerialNumber() {
        return connectionSerialNumber;
    }

    public int getVendorId() {
        return vendorId;
    }

    public long getVendorSerialNumber() {
        return vendorSerialNumber;
    }

    public int getConnectionTimeoutMultiplier() {
        return connectionTimeoutMultiplier;
    }

    public EPath.PaddedEPath getConnectionPath() {
        return connectionPath;
    }

    public Duration getO2tRpi() {
        return o2tRpi;
    }

    public NetworkConnectionParameters getO2tParameters() {
        return o2tParameters;
    }

    public Duration getT2oRpi() {
        return t2oRpi;
    }

    public NetworkConnectionParameters getT2oParameters() {
        return t2oParameters;
    }

    public int getTransportClassAndTrigger() {
        return transportClassAndTrigger;
    }

    public static ByteBuf encode(ForwardOpenRequest request, ByteBuf buffer) {
        int priorityAndTimeoutBytes = TimeoutCalculator.calculateTimeoutBytes(request.timeout);
        buffer.writeByte(priorityAndTimeoutBytes >> 8 & 0xFF);
        buffer.writeByte(priorityAndTimeoutBytes & 0xFF);

        buffer.writeInt(0); // o2tConnectionId chosen by remote and indicated in response
        buffer.writeInt(request.t2oConnectionId);
        buffer.writeShort(request.connectionSerialNumber);

        buffer.writeShort(request.vendorId);
        buffer.writeInt((int) request.vendorSerialNumber);

        buffer.writeByte(request.connectionTimeoutMultiplier);
        buffer.writeZero(3); // 3 reserved bytes

        buffer.writeInt((int) TimeUnit.MICROSECONDS.convert(request.o2tRpi.toMillis(), TimeUnit.MILLISECONDS));
        buffer.writeShort(parametersToInt(request.o2tParameters));

        buffer.writeInt((int) TimeUnit.MICROSECONDS.convert(request.t2oRpi.toMillis(), TimeUnit.MILLISECONDS));
        buffer.writeShort(parametersToInt(request.t2oParameters));

        buffer.writeByte(request.transportClassAndTrigger);

        EPath.PaddedEPath.encode(request.connectionPath, buffer);

        return buffer;
    }

    private static int parametersToInt(NetworkConnectionParameters parameters) {
        int parametersInt = parameters.getConnectionSize() & 0x1FF;

        parametersInt |= (parameters.getSizeType().ordinal() << 9);
        parametersInt |= (parameters.getPriority().ordinal() << 10);
        parametersInt |= (parameters.getConnectionType().ordinal() << 13);
        if (parameters.isRedundantOwner()) parametersInt |= (1 << 15);

        return parametersInt;
    }

} public final class ForwardOpenResponse {

    private final int o2tConnectionId;
    private final int t2oConnectionId;
    private final int connectionSerialNumber;
    private final int originatorVendorId;
    private final long originatorSerialNumber;
    private final Duration o2tApi;
    private final Duration t2oApi;
    private final int applicationReplySize;
    private final ByteBuf applicationReply;

    public ForwardOpenResponse(int o2tConnectionId,
                               int t2oConnectionId,
                               int connectionSerialNumber,
                               int originatorVendorId,
                               long originatorSerialNumber,
                               Duration o2tApi,
                               Duration t2oApi,
                               int applicationReplySize,
                               ByteBuf applicationReply) {

        this.o2tConnectionId = o2tConnectionId;
        this.t2oConnectionId = t2oConnectionId;
        this.connectionSerialNumber = connectionSerialNumber;
        this.originatorVendorId = originatorVendorId;
        this.originatorSerialNumber = originatorSerialNumber;
        this.o2tApi = o2tApi;
        this.t2oApi = t2oApi;
        this.applicationReplySize = applicationReplySize;
        this.applicationReply = applicationReply;
    }

    public int getO2tConnectionId() {
        return o2tConnectionId;
    }

    public int getT2oConnectionId() {
        return t2oConnectionId;
    }

    public int getConnectionSerialNumber() {
        return connectionSerialNumber;
    }

    public int getOriginatorVendorId() {
        return originatorVendorId;
    }

    public long getOriginatorSerialNumber() {
        return originatorSerialNumber;
    }

    public Duration getO2tApi() {
        return o2tApi;
    }

    public Duration getT2oApi() {
        return t2oApi;
    }

    public int getApplicationReplySize() {
        return applicationReplySize;
    }

    public ByteBuf getApplicationReply() {
        return applicationReply;
    }

    public static ForwardOpenResponse decode(ByteBuf buffer) {
        int o2tConnectionId = buffer.readInt();
        int t2oConnectionId = buffer.readInt();
        int connectionSerialNumber = buffer.readUnsignedShort();
        int originatorVendorId = buffer.readUnsignedShort();
        long originatorSerialNumber = buffer.readUnsignedInt();
        long o2tActualPacketInterval = TimeUnit.MICROSECONDS
            .convert(buffer.readUnsignedInt(), TimeUnit.MILLISECONDS);
        long t2oActualPacketInterval = TimeUnit.MICROSECONDS
            .convert(buffer.readUnsignedInt(), TimeUnit.MILLISECONDS);
        int applicationReplySize = buffer.readUnsignedByte();
        buffer.skipBytes(1); // reserved

        ByteBuf applicationReply = applicationReplySize > 0 ?
            buffer.readSlice(applicationReplySize).copy() :
            Unpooled.EMPTY_BUFFER;

        return new ForwardOpenResponse(
            o2tConnectionId,
            t2oConnectionId,
            connectionSerialNumber,
            originatorVendorId,
            originatorSerialNumber,
            Duration.ofMillis(o2tActualPacketInterval),
            Duration.ofMillis(t2oActualPacketInterval),
            applicationReplySize,
            applicationReply
        );
    }

} public class LargeForwardOpenRequest {

    private final Duration timeout;
    private final int o2tConnectionId;
    private final int t2oConnectionId;
    private final int connectionSerialNumber;
    private final int vendorId;
    private final long vendorSerialNumber;
    private final int connectionTimeoutMultiplier;
    private final EPath.PaddedEPath connectionPath;
    private final Duration o2tRpi;
    private final NetworkConnectionParameters o2tParameters;
    private final Duration t2oRpi;
    private final NetworkConnectionParameters t2oParameters;
    private final int transportClassAndTrigger;

    public LargeForwardOpenRequest(Duration timeout,
                                   int o2tConnectionId,
                                   int t2oConnectionId,
                                   int connectionSerialNumber,
                                   int vendorId,
                                   long vendorSerialNumber,
                                   int connectionTimeoutMultiplier,
                                   PaddedEPath connectionPath,
                                   Duration o2tRpi,
                                   NetworkConnectionParameters o2tParameters,
                                   Duration t2oRpi,
                                   NetworkConnectionParameters t2oParameters,
                                   int transportClassAndTrigger) {

        this.timeout = timeout;
        this.o2tConnectionId = o2tConnectionId;
        this.t2oConnectionId = t2oConnectionId;
        this.connectionSerialNumber = connectionSerialNumber;
        this.vendorId = vendorId;
        this.vendorSerialNumber = vendorSerialNumber;
        this.connectionTimeoutMultiplier = connectionTimeoutMultiplier;
        this.connectionPath = connectionPath;
        this.o2tRpi = o2tRpi;
        this.o2tParameters = o2tParameters;
        this.t2oRpi = t2oRpi;
        this.t2oParameters = t2oParameters;
        this.transportClassAndTrigger = transportClassAndTrigger;
    }

    public Duration getTimeout() {
        return timeout;
    }

    public int getO2tConnectionId() {
        return o2tConnectionId;
    }

    public int getT2oConnectionId() {
        return t2oConnectionId;
    }

    public int getConnectionSerialNumber() {
        return connectionSerialNumber;
    }

    public int getVendorId() {
        return vendorId;
    }

    public long getVendorSerialNumber() {
        return vendorSerialNumber;
    }

    public int getConnectionTimeoutMultiplier() {
        return connectionTimeoutMultiplier;
    }

    public PaddedEPath getConnectionPath() {
        return connectionPath;
    }

    public Duration getO2tRpi() {
        return o2tRpi;
    }

    public NetworkConnectionParameters getO2tParameters() {
        return o2tParameters;
    }

    public Duration getT2oRpi() {
        return t2oRpi;
    }

    public NetworkConnectionParameters getT2oParameters() {
        return t2oParameters;
    }

    public int getTransportClassAndTrigger() {
        return transportClassAndTrigger;
    }

    public static ByteBuf encode(LargeForwardOpenRequest request, ByteBuf buffer) {
        int priorityAndTimeoutBytes = TimeoutCalculator.calculateTimeoutBytes(request.timeout);
        buffer.writeByte(priorityAndTimeoutBytes >> 8 & 0xFF);
        buffer.writeByte(priorityAndTimeoutBytes & 0xFF);

        buffer.writeInt(0); // o2tConnectionId chosen by remote and indicated in response
        buffer.writeInt(request.t2oConnectionId);
        buffer.writeShort(request.connectionSerialNumber);

        buffer.writeShort(request.vendorId);
        buffer.writeInt((int) request.vendorSerialNumber);

        buffer.writeByte(request.connectionTimeoutMultiplier);
        buffer.writeZero(3); // 3 reserved bytes

        buffer.writeInt((int) TimeUnit.MICROSECONDS.convert(request.o2tRpi.toMillis(), TimeUnit.MILLISECONDS));
        buffer.writeInt(parametersToInt(request.o2tParameters));

        buffer.writeInt((int) TimeUnit.MICROSECONDS.convert(request.t2oRpi.toMillis(), TimeUnit.MILLISECONDS));
        buffer.writeInt(parametersToInt(request.t2oParameters));

        buffer.writeByte(request.transportClassAndTrigger);

        EPath.PaddedEPath.encode(request.connectionPath, buffer);

        return buffer;
    }

    private static int parametersToInt(NetworkConnectionParameters parameters) {
        int parametersInt = parameters.getConnectionSize() & 0xFFFF;

        parametersInt |= (parameters.getSizeType().ordinal() << 25);
        parametersInt |= (parameters.getPriority().ordinal() << 26);
        parametersInt |= (parameters.getConnectionType().ordinal() << 29);
        if (parameters.isRedundantOwner()) parametersInt |= (1 << 31);

        return parametersInt;
    }

} public class LargeForwardOpenResponse {

    private final int o2tConnectionId;
    private final int t2oConnectionId;
    private final int connectionSerialNumber;
    private final int originatorVendorId;
    private final long originatorSerialNumber;
    private final Duration o2tApi;
    private final Duration t2oApi;
    private final int applicationReplySize;
    private final ByteBuf applicationReply;

    public LargeForwardOpenResponse(int o2tConnectionId,
                                    int t2oConnectionId,
                                    int connectionSerialNumber,
                                    int originatorVendorId,
                                    long originatorSerialNumber,
                                    Duration o2tApi,
                                    Duration t2oApi,
                                    int applicationReplySize,
                                    ByteBuf applicationReply) {

        this.o2tConnectionId = o2tConnectionId;
        this.t2oConnectionId = t2oConnectionId;
        this.connectionSerialNumber = connectionSerialNumber;
        this.originatorVendorId = originatorVendorId;
        this.originatorSerialNumber = originatorSerialNumber;
        this.o2tApi = o2tApi;
        this.t2oApi = t2oApi;
        this.applicationReplySize = applicationReplySize;
        this.applicationReply = applicationReply;
    }

    public int getO2tConnectionId() {
        return o2tConnectionId;
    }

    public int getT2oConnectionId() {
        return t2oConnectionId;
    }

    public int getConnectionSerialNumber() {
        return connectionSerialNumber;
    }

    public int getOriginatorVendorId() {
        return originatorVendorId;
    }

    public long getOriginatorSerialNumber() {
        return originatorSerialNumber;
    }

    public Duration getO2tApi() {
        return o2tApi;
    }

    public Duration getT2oApi() {
        return t2oApi;
    }

    public int getApplicationReplySize() {
        return applicationReplySize;
    }

    public ByteBuf getApplicationReply() {
        return applicationReply;
    }

    public static LargeForwardOpenResponse decode(ByteBuf buffer) {
        int o2tConnectionId = buffer.readInt();
        int t2oConnectionId = buffer.readInt();
        int connectionSerialNumber = buffer.readUnsignedShort();
        int originatorVendorId = buffer.readUnsignedShort();
        long originatorSerialNumber = buffer.readUnsignedInt();
        long o2tActualPacketInterval = TimeUnit.MICROSECONDS
            .convert(buffer.readUnsignedInt(), TimeUnit.MILLISECONDS);
        long t2oActualPacketInterval = TimeUnit.MICROSECONDS
            .convert(buffer.readUnsignedInt(), TimeUnit.MILLISECONDS);
        int applicationReplySize = buffer.readUnsignedByte();
        buffer.skipBytes(1); // reserved

        ByteBuf applicationReply = applicationReplySize > 0 ?
            buffer.readSlice(applicationReplySize).copy() :
            Unpooled.EMPTY_BUFFER;

        return new LargeForwardOpenResponse(
            o2tConnectionId,
            t2oConnectionId,
            connectionSerialNumber,
            originatorVendorId,
            originatorSerialNumber,
            Duration.ofMillis(o2tActualPacketInterval),
            Duration.ofMillis(t2oActualPacketInterval),
            applicationReplySize,
            applicationReply
        );
    }

} public class NetworkConnectionParameters {

    private final int connectionSize;
    private final SizeType sizeType;
    private final Priority priority;
    private final ConnectionType connectionType;
    private final boolean redundantOwner;

    public NetworkConnectionParameters(int connectionSize,
                                       SizeType sizeType,
                                       Priority priority,
                                       ConnectionType connectionType,
                                       boolean redundantOwner) {

        this.connectionSize = connectionSize;
        this.sizeType = sizeType;
        this.priority = priority;
        this.connectionType = connectionType;
        this.redundantOwner = redundantOwner;
    }

    public int getConnectionSize() {
        return connectionSize;
    }

    public SizeType getSizeType() {
        return sizeType;
    }

    public Priority getPriority() {
        return priority;
    }

    public ConnectionType getConnectionType() {
        return connectionType;
    }

    public boolean isRedundantOwner() {
        return redundantOwner;
    }

    public static enum SizeType {
        Fixed,
        Variable
    }

    public static enum Priority {
        Low,
        High,
        Scheduled,
        Urgent
    }

    public static enum ConnectionType {
        Null,
        Multicast,
        PointToPoint,
        Reserved
    }

}
import java.time.Duration;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import com.digitalpetri.enip.cip.epath.EPath.PaddedEPath;
import com.digitalpetri.enip.cip.epath.LogicalSegment.ClassId;
import com.digitalpetri.enip.cip.epath.LogicalSegment.InstanceId;
import com.digitalpetri.enip.cip.services.CipService.PartialResponseException;
import com.digitalpetri.enip.cip.services.ForwardCloseService;
import com.digitalpetri.enip.cip.services.ForwardOpenService;
import com.digitalpetri.enip.cip.services.LargeForwardOpenService;
import com.digitalpetri.enip.cip.structs.ForwardCloseRequest;
import com.digitalpetri.enip.cip.structs.ForwardCloseResponse;
import com.digitalpetri.enip.cip.structs.ForwardOpenRequest;
import com.digitalpetri.enip.cip.structs.ForwardOpenResponse;
import com.digitalpetri.enip.cip.structs.LargeForwardOpenRequest;
import com.digitalpetri.enip.cip.structs.LargeForwardOpenResponse;
import com.digitalpetri.enip.cip.structs.NetworkConnectionParameters;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
        CompletableFuture<CipConnection> future = new CompletableFuture<>();

        acquire0().whenComplete((c, ex) -> {
            if (c != null) {
                if (c.isExpired()) {
                    remove(c);

                    acquire().whenComplete((c2, ex2) -> {
                        if (c2 != null) future.complete(c2);
                        else future.completeExceptionally(ex2);
                    });
                } else {
                    future.complete(c);
                }
            } else {
                future.completeExceptionally(ex);
            }
        });

        return future;
    }

    private synchronized CompletableFuture<CipConnection> acquire0() {
        CompletableFuture<CipConnection> future = new CompletableFuture<>();

        if (!queue.isEmpty()) {
            future.complete(queue.poll());
        } else {
            waitQueue.add(future);

            if (count.incrementAndGet() <= connectionLimit) {
                CompletableFuture<CipConnection> f = connectionFactory.open();

                f.whenComplete((c, ex) -> {
                    CompletableFuture<CipConnection> waiter;
                    synchronized (CipConnectionPool.this) {
                        waiter = waitQueue.poll();
                    }

                    if (c != null) {
                        if (waiter != null) {
                            waiter.complete(c);
                        } else {
                            queue.add(c);
                        }
                        loggingContext.forEach(MDC::put);
                        try {
                            logger.debug("Forward open succeeded: {}", c);
                        } finally {
                            loggingContext.keySet().forEach(MDC::remove);
                        }
                    } else {
                        count.decrementAndGet();
                        if (waiter != null) waiter.completeExceptionally(ex);

                        loggingContext.forEach(MDC::put);
                        try {
                            logger.debug("Forward open failed: {}", ex.getMessage(), ex);
                        } finally {
                            loggingContext.keySet().forEach(MDC::remove);
                        }
                    }
                });
            } else {
                count.decrementAndGet();
            }
        }

        return future;
    }

    public synchronized void release(CipConnection connection) {
        connection.updateLastUse();

        if (!waitQueue.isEmpty()) {
            waitQueue.poll().complete(connection);
        } else {
            queue.add(connection);
        }
    }

    public synchronized void remove(CipConnection connection) {
        connectionFactory.close(connection).thenRun(
            () -> {
                loggingContext.forEach(MDC::put);
                try {
                    logger.debug("Connection closed: {}", connection);
                } finally {
                    loggingContext.keySet().forEach(MDC::remove);
                }
            }
        );

        queue.remove(connection);
        count.decrementAndGet();

        if (!waitQueue.isEmpty()) {
            CompletableFuture<CipConnection> next = waitQueue.poll();

            acquire().whenComplete((c, ex) -> {
                if (c != null) next.complete(c);
                else next.completeExceptionally(ex);
            });
        }
    }

    public interface CipConnectionFactory {

        CompletableFuture<CipConnection> open();

        CompletableFuture<ForwardCloseResponse> close(CipConnection connection);

    }

    public static class DefaultConnectionFactory implements CipConnectionFactory {

        private static final Duration DEFAULT_RPI = Duration.ofSeconds(2);
        private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(15);

        private static final PaddedEPath MESSAGE_ROUTER_CP_PATH = new PaddedEPath(
            new ClassId(0x02),