public async Task <uint> SendReliableMessage(
            string destination,
            byte[] data,
            IProgress <OutMessageProgressInfo> progress = null,
            uint messageId          = 0,
            uint replyToMessageId   = 0,
            byte messageKind        = 0,
            CancellationToken token = default(CancellationToken)
            )
        {
            using (_stateHelper.GetFuncWrapper())
            {
                try
                {
                    Assert.True(SamHelper.IsDestinationStringValid(destination));
                    Assert.NotNull(data);
                    Assert.InRange(data.Length, 1, _settings.MaxMesageLength);
                    if (messageId == 0)
                    {
                        messageId = await GetNextOutMessageId().ConfigureAwait(false);
                    }
                    DestinationInfo destinationInfo;
                    using (await _destinationDbLockSem.GetDisposable(token: token).ConfigureAwait(false))
                    {
                        if (!_destinationDb.ContainsKey(destination))
                        {
                            _destinationDb.Add(destination, new DestinationInfo());
                        }
                        destinationInfo = _destinationDb[destination];
                    }
                    OutMessageInfo outMessageInfo;
                    using (await destinationInfo.OutMessagesDbLockSem.GetDisposable(token: token).ConfigureAwait(false))
                    {
                        if (
                            destinationInfo.OutMessagesDb.ContainsKey(messageId)
                            )
                        {
                            throw new EnumException <SendReliableMessageExcs>(
                                      SendReliableMessageExcs.ExistMessageId
                                      );
                        }
                        outMessageInfo = new OutMessageInfo(
                            data,
                            replyToMessageId,
                            _settings.MaxBlockSize,
                            progress
                            );
                        destinationInfo.OutMessagesDb.Add(messageId, outMessageInfo);
                    }
                    try
                    {
                        uint blocksCount = outMessageInfo.BlockCount;

                        /*
                         * Handshake
                         */
                        using (
                            var compositeTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
                                _cts.Token,
                                token
                                )
                            )
                        {
                            try
                            {
                                var messageStatusTask =
                                    MessageStatusReceived
                                    .Where(
                                        x =>
                                        x.Destination == destination &&
                                        x.MessageId == messageId
                                        )
                                    .FirstAsync()
                                    .ToTask(compositeTokenSource.Token);
                                byte[] messageHash = GetMessageHash(
                                    messageId,
                                    replyToMessageId,
                                    messageKind,
                                    data
                                    );
                                for (
                                    outMessageInfo.HandshakeAttemptNo = 0;
                                    outMessageInfo.HandshakeAttemptNo
                                    <= _settings.HandshakeAttemptCount;
                                    outMessageInfo.HandshakeAttemptNo++
                                    )
                                {
                                    if (
                                        outMessageInfo.HandshakeAttemptNo
                                        == _settings.HandshakeAttemptCount
                                        )
                                    {
                                        throw new EnumException <SendReliableMessageExcs>(
                                                  SendReliableMessageExcs.HandshakeTimeout
                                                  );
                                    }
                                    await SendHandshakeStart(
                                        destination,
                                        messageId,
                                        replyToMessageId,
                                        (uint)data.Length,
                                        outMessageInfo.BlockSize,
                                        messageHash,
                                        outMessageInfo.Data[0],
                                        messageKind
                                        ).ConfigureAwait(false);

                                    var waitTasks = await Task.WhenAny(
                                        messageStatusTask,
                                        Task.Delay(
                                            TimeSpan.FromSeconds(
                                                _settings.HandshakeTimeoutSeconds
                                                ),
                                            compositeTokenSource.Token
                                            )
                                        ).ConfigureAwait(false);

                                    if (waitTasks == messageStatusTask)
                                    {
                                        var messageStatus = await messageStatusTask.ConfigureAwait(false);

                                        if (
                                            !(
                                                messageStatus.StatusCode ==
                                                MessageStatusArgs.MessageStatusCode.HandshakeOk
                                                ||
                                                (
                                                    messageStatus.StatusCode ==
                                                    MessageStatusArgs.MessageStatusCode.AllBlocksReceived &&
                                                    outMessageInfo.BlockCount == 1
                                                )
                                                )
                                            )
                                        {
                                            throw new EnumException <SendReliableMessageExcs>(
                                                      SendReliableMessageExcs.HandshakeError,
                                                      messageStatus.WriteObjectToJson()
                                                      );
                                        }
                                        break;
                                    }
                                }
                                outMessageInfo.Status            = OutMessageStatus.HandshakeOk;
                                outMessageInfo.BlockConfirmed[0] = true;
                                if (outMessageInfo.BlockCount > 1)
                                {
                                    // Handshake established, start sending blocks
                                    DateTime lastBlockConfirmationTime = DateTime.UtcNow;
                                    var      blocksInProgress          = new SortedSet <uint>();
                                    var      blocksInProgressLockSem   = new SemaphoreSlim(1);
                                    var      blockSentTimes            = new DateTime[blocksCount];
                                    using (
                                        BlockConfirmationReceived
                                        .Where(_ =>
                                               _.Destination == destination &&
                                               _.MessageId == messageId
                                               )
                                        .ObserveOn(TaskPoolScheduler.Default)
                                        .Subscribe(async x =>
                                    {
                                        if (
                                            x.BlockId < blocksCount &&
                                            !outMessageInfo.BlockConfirmed[x.BlockId]
                                            )
                                        {
                                            await outMessageInfo.ConfirmBlock(x.BlockId).ConfigureAwait(false);
                                            lastBlockConfirmationTime = DateTime.UtcNow;
                                            using (await blocksInProgressLockSem.GetDisposable().ConfigureAwait(false))
                                            {
                                                blocksInProgress.Remove(x.BlockId);
                                            }
                                        }
                                    }
                                                   )
                                        )
                                    {
                                        while (true)
                                        {
                                            compositeTokenSource.Token.ThrowIfCancellationRequested();
                                            IEnumerable <int> blocksToSend;
                                            using (
                                                await
                                                blocksInProgressLockSem.GetDisposable(token: compositeTokenSource.Token)
                                                .ConfigureAwait(false))
                                            {
                                                if (
                                                    blocksInProgress.Count == 0 &&
                                                    outMessageInfo.BlockConfirmed.All(x => x)
                                                    )
                                                {
                                                    break;
                                                }
                                                blocksInProgress.RemoveWhere(
                                                    i =>
                                                    DateTime.UtcNow >
                                                    (
                                                        blockSentTimes[i]
                                                        + TimeSpan.FromSeconds(
                                                            _settings
                                                            .ConfirmationOneBlockTimeoutSeconds
                                                            )
                                                    )
                                                    );
                                                blocksToSend = Enumerable.Range(0, (int)blocksCount)
                                                               .Where(
                                                    i =>
                                                    !blocksInProgress.Contains((uint)i) &&
                                                    !outMessageInfo.BlockConfirmed[i]
                                                    )
                                                               .Take(
                                                    Math.Max(
                                                        0,
                                                        _settings.WindowSize
                                                        - blocksInProgress.Count
                                                        )
                                                    );
                                            }
                                            foreach (int blockId in blocksToSend)
                                            {
                                                await SendBlock(
                                                    destination,
                                                    messageId,
                                                    (uint)blockId
                                                    ).ConfigureAwait(false);

                                                await outMessageInfo.SendBlock(
                                                    (uint)blockId
                                                    ).ConfigureAwait(false);

                                                using (
                                                    await
                                                    blocksInProgressLockSem.GetDisposable(token: compositeTokenSource.Token)
                                                    .ConfigureAwait(false))
                                                {
                                                    blocksInProgress.Add((uint)blockId);
                                                }
                                                blockSentTimes[blockId] = DateTime.UtcNow;
                                            }
                                            if (
                                                DateTime.UtcNow >
                                                lastBlockConfirmationTime +
                                                TimeSpan.FromSeconds(
                                                    _settings.ConfirmationTimeoutSeconds
                                                    )
                                                )
                                            {
                                                throw new EnumException <SendReliableMessageExcs>(
                                                          SendReliableMessageExcs.SendBlocksTimeout
                                                          );
                                            }
                                            await Task.Delay(50, compositeTokenSource.Token).ConfigureAwait(false);
                                        }
                                    }
                                }
                                outMessageInfo.Status = OutMessageStatus.AllBlocksSent;
                                // all blocks sent and confirmed
                                _log.Trace(
                                    "{4}, send reliable message of {0} bytes " +
                                    "to {1} id={2},rid={3},kind={5}",
                                    data.Length,
                                    destination.Substring(0, 20),
                                    messageId,
                                    replyToMessageId,
                                    _reliableSamHelperGuid.ToString().Substring(0, 5),
                                    messageKind
                                    );
                                return(messageId);
                            }
                            finally
                            {
                                compositeTokenSource.Cancel();
                            }
                        }
                    }
                    finally
                    {
                        TryRemoveOutMessageInfo(destinationInfo, messageId);
                    }
                }
                catch (EnumException <SendReliableMessageExcs> )
                {
                    throw;
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch (TimeoutException)
                {
                    throw;
                }
                catch (Exception exc)
                {
                    throw new EnumException <SendReliableMessageExcs>(
                              SendReliableMessageExcs.UnexpectedError,
                              innerException: exc
                              );
                }
            }
        }
        private async void SamHelperOnDatagramDataReceived(
            DatagramDataReceivedArgs datagramDataReceivedArgs
            )
        {
            try
            {
                using (_stateHelper.GetFuncWrapper())
                {
                    if (
                        datagramDataReceivedArgs.Data.Length
                        > (_settings.MaxBlockSize + 1000)
                        )
                    {
                        return;
                    }
                    if (
                        _dropDestinations.Contains(
                            datagramDataReceivedArgs.Destination
                            )
                        )
                    {
                        return;
                    }

                    string fromDestination = datagramDataReceivedArgs.Destination;
                    using (
                        var reader = new EndianBinaryReader(
                            _littleEndianConverter,
                            new MemoryStream(datagramDataReceivedArgs.Data)
                            )
                        )
                    {
                        var blockType = (ReliableDatagramCodes)reader.ReadByte();
                        switch (blockType)
                        {
                        case ReliableDatagramCodes.RawDatagram:
                        {
                            uint messageId        = reader.ReadUInt32();
                            uint replyToMessageId = reader.ReadUInt32();
                            byte messageKind      = reader.ReadByte();
                            var  messageHash      = reader.ReadBytesOrThrow(32);
                            var  rawData          = ReadBytesToEnd(reader);
                            if (
                                messageHash.SequenceEqual(
                                    GetMessageHash(
                                        messageId,
                                        replyToMessageId,
                                        messageKind,
                                        rawData
                                        )
                                    )
                                )
                            {
                                RawDatagramReceived.OnNext(
                                    new ReliableMessageReceivedArgs()
                                    {
                                        Data             = rawData,
                                        Destination      = fromDestination,
                                        MessageId        = messageId,
                                        ReplyToMessageId = replyToMessageId,
                                        MessageKind      = messageKind
                                    }
                                    );
                            }
                            return;
                        }

                        case ReliableDatagramCodes.HandshakeStart:
                        {
                            uint messageId        = reader.ReadUInt32();
                            uint replyToMessageId = reader.ReadUInt32();
                            byte messageKind      = reader.ReadByte();
                            uint totalMessageSize = reader.ReadUInt32();
                            if (totalMessageSize > _settings.MaxMesageLength)
                            {
                                return;
                            }
                            uint blockSize = reader.ReadUInt32();
                            if (blockSize > _settings.MaxBlockSize)
                            {
                                return;
                            }
                            var  messageHash    = reader.ReadBytesOrThrow(32);
                            uint firstBlockSize = reader.ReadUInt32();
                            if (
                                firstBlockSize == 0
                                ||
                                firstBlockSize > _settings.MaxBlockSize
                                )
                            {
                                return;
                            }
                            byte[] firstBlockData
                                = reader.ReadBytesOrThrow((int)firstBlockSize);
                            HandshakeStartReceived.OnNext(
                                new HandshakeStartArgs
                                {
                                    Destination      = fromDestination,
                                    BlockSize        = blockSize,
                                    MessageId        = messageId,
                                    ReplyToMessageId = replyToMessageId,
                                    MessageSize      = totalMessageSize,
                                    MessageKind      = messageKind,
                                    MessageHash      = messageHash,
                                    FirstBlockData   = firstBlockData
                                }
                                );
                            return;
                        }

                        case ReliableDatagramCodes.MessageStatus:
                        {
                            uint messageId = reader.ReadUInt32();
                            if (!await CheckExistOutMessage(fromDestination, messageId).ConfigureAwait(false))
                            {
                                return;
                            }
                            var errorCode
                                = (MessageStatusArgs.MessageStatusCode)
                                  reader.ReadByte();
                            uint blocksReceived = reader.ReadUInt32();
                            MessageStatusReceived.OnNext(
                                new MessageStatusArgs()
                                {
                                    Destination
                                        = datagramDataReceivedArgs
                                          .Destination,
                                    MessageId      = messageId,
                                    StatusCode     = errorCode,
                                    BlocksReceived = blocksReceived
                                }
                                );
                            return;
                        }

                        case ReliableDatagramCodes.BlockSend:
                        {
                            uint messageId = reader.ReadUInt32();
                            uint blockId   = reader.ReadUInt32();
                            var  blockData = ReadBytesToEnd(reader);
                            if (
                                blockData.Length == 0 ||
                                blockData.Length > _settings.MaxBlockSize
                                )
                            {
                                return;
                            }
                            BlockSendReceived.OnNext(new BlockSendArgs()
                                {
                                    Destination = fromDestination,
                                    MessageId   = messageId,
                                    BlockId     = blockId,
                                    BlockData   = blockData
                                });
                            return;
                        }

                        case ReliableDatagramCodes.BlockConfirmation:
                        {
                            uint messageId = reader.ReadUInt32();
                            if (!await CheckExistOutMessage(fromDestination, messageId).ConfigureAwait(false))
                            {
                                return;
                            }
                            uint blockId = reader.ReadUInt32();
                            BlockConfirmationReceived.OnNext(
                                new BlockConfirmationArgs()
                                {
                                    Destination = fromDestination,
                                    MessageId   = messageId,
                                    BlockId     = blockId
                                }
                                );
                            return;
                        }

                        case ReliableDatagramCodes.GetMessageStatus:
                        {
                            uint messageId = reader.ReadUInt32();
                            GetMessageStatusReceived.OnNext(
                                new GetMessageStatusArgs()
                                {
                                    Destination = fromDestination,
                                    MessageId   = messageId
                                }
                                );
                            return;
                        }

                        case ReliableDatagramCodes.GetProtocolVersion:
                        {
                            GetProtocolVersionReceived.OnNext(
                                new GetProtocolVersionArgs()
                                {
                                    Destination = fromDestination
                                }
                                );
                            return;
                        }

                        case ReliableDatagramCodes.ProtocolVersion:
                        {
                            uint protocolVersion = reader.ReadUInt32();
                            ProtocolVersionReceived.OnNext(
                                new ProtocolVersionArgs()
                                {
                                    Destination     = fromDestination,
                                    ProtocolVersion = protocolVersion
                                }
                                );
                            return;
                        }
                        }
                    }
                }
            }
            catch (EndOfStreamException exc)
            {
                _log.Error(
                    "SamHelperOnDatagramDataReceived" +
                    " EndOfStreamException '{0}'",
                    exc.ToString()
                    );
            }
            catch (OperationCanceledException)
            {
            }
            catch (WrongDisposableObjectStateException)
            {
            }
            catch (Exception exc)
            {
                _log.Error(
                    "SamHelperOnDatagramDataReceived" +
                    " unexpected error '{0}'",
                    exc.ToString()
                    );
            }
        }