// Kcp based on https://github.com/skywind3000/kcp
// Kept as close to original as possible.
using System;
using System.Collections.Generic;

namespace IGP.UnitySDK.ThirdParty.Kcp2k
{
    public class Kcp
    {
        // original Kcp has a define option, which is not defined by default:
        // #define FASTACK_CONSERVE

        public const int RTO_NDL = 30;             // no delay min rto
        public const int RTO_MIN = 100;            // normal min rto
        public const int RTO_DEF = 200;            // default RTO
        public const int RTO_MAX = 60000;          // maximum RTO
        public const int CMD_PUSH = 81;            // cmd: push data
        public const int CMD_ACK  = 82;            // cmd: ack
        public const int CMD_WASK = 83;            // cmd: window probe (ask)
        public const int CMD_WINS = 84;            // cmd: window size (tell/insert)
        public const int ASK_SEND = 1;             // need to send CMD_WASK
        public const int ASK_TELL = 2;             // need to send CMD_WINS
        public const int WND_SND = 32;             // default send window
        public const int WND_RCV = 128;            // default receive window. must be >= max fragment size
        public const int MTU_DEF = 1200;           // default MTU (reduced to 1200 to fit all cases: https://en.wikipedia.org/wiki/Maximum_transmission_unit ; steam uses 1200 too!)
        public const int ACK_FAST = 3;
        public const int INTERVAL = 100;
        public const int OVERHEAD = 24;
        public const int FRG_MAX = byte.MaxValue;  // kcp encodes 'frg' as byte. so we can only ever send up to 255 fragments.
        public const int DEADLINK = 20;            // default maximum amount of 'xmit' retransmissions until a segment is considered lost
        public const int THRESH_INIT = 2;
        public const int THRESH_MIN = 2;
        public const int PROBE_INIT = 7000;        // 7 secs to probe window size
        public const int PROBE_LIMIT = 120000;     // up to 120 secs to probe window
        public const int FASTACK_LIMIT = 5;        // max times to trigger fastack

        // kcp members.
        internal int state;
        readonly uint conv;          // conversation
        internal uint mtu;
        internal uint mss;           // maximum segment size := MTU - OVERHEAD
        internal uint snd_una;       // unacknowledged. e.g. snd_una is 9 it means 8 has been confirmed, 9 and 10 have been sent
        internal uint snd_nxt;       // forever growing send counter for sequence numbers
        internal uint rcv_nxt;       // forever growing receive counter for sequence numbers
        internal uint ssthresh;      // slow start threshold
        internal int rx_rttval;      // average deviation of rtt, used to measure the jitter of rtt
        internal int rx_srtt;        // smoothed round trip time (a weighted average of rtt)
        internal int rx_rto;
        internal int rx_minrto;
        internal uint snd_wnd;       // send window
        internal uint rcv_wnd;       // receive window
        internal uint rmt_wnd;       // remote window
        internal uint cwnd;          // congestion window
        internal uint probe;
        internal uint interval;
        internal uint ts_flush;      // last flush timestamp in milliseconds
        internal uint xmit;
        internal uint nodelay;       // not a bool. original Kcp has '<2 else' check.
        internal bool updated;
        internal uint ts_probe;      // probe timestamp
        internal uint probe_wait;
        internal uint dead_link;     // maximum amount of 'xmit' retransmissions until a segment is considered lost
        internal uint incr;
        internal uint current;       // current time (milliseconds). set by Update.

        internal int fastresend;
        internal int fastlimit;
        internal bool nocwnd;        // congestion control, negated. heavily restricts send/recv window sizes.
        internal readonly Queue<Segment> snd_queue = new Queue<Segment>(16); // send queue
        internal readonly Queue<Segment> rcv_queue = new Queue<Segment>(16); // receive queue
        // snd_buffer needs index removals.
        // C# LinkedList allocates for each entry, so let's keep List for now.
        internal readonly List<Segment> snd_buf = new List<Segment>(16);   // send buffer
        // rcv_buffer needs index insertions and backwards iteration.
        // C# LinkedList allocates for each entry, so let's keep List for now.
        internal readonly List<Segment> rcv_buf = new List<Segment>(16);   // receive buffer
        internal readonly List<AckItem> acklist = new List<AckItem>(16);

        // memory buffer
        // size depends on MTU.
        // MTU can be changed at runtime, which resizes the buffer.
        internal byte[] buffer;

        // output function of type <buffer, size>
        readonly Action<byte[], int> output;

        // segment pool to avoid allocations in C#.
        // this is not part of the original C code.
        readonly Pool<Segment> SegmentPool = new Pool<Segment>(
            // create new segment
            () => new Segment(),
            // reset segment before reuse
            (segment) => segment.Reset(),
            // initial capacity
            32
        );

        // ikcp_create
        // create a new kcp control object, 'conv' must equal in two endpoint
        // from the same connection.
        public Kcp(uint conv, Action<byte[], int> output)
        {
            this.conv   = conv;
            this.output = output;
            snd_wnd = WND_SND;
            rcv_wnd = WND_RCV;
            rmt_wnd = WND_RCV;
            mtu = MTU_DEF;
            mss = mtu - OVERHEAD;
            rx_rto    = RTO_DEF;
            rx_minrto = RTO_MIN;
            interval  = INTERVAL;
            ts_flush  = INTERVAL;
            ssthresh  = THRESH_INIT;
            fastlimit = FASTACK_LIMIT;
            dead_link = DEADLINK;
            buffer = new byte[(mtu + OVERHEAD) * 3];
        }

        // ikcp_segment_new
        // we keep the original function and add our pooling to it.
        // this way we'll never miss it anywhere.
        Segment SegmentNew() => SegmentPool.Take();

        // ikcp_segment_delete
        // we keep the original function and add our pooling to it.
        // this way we'll never miss it anywhere.
        void SegmentDelete(Segment seg) => SegmentPool.Return(seg);

        // calculate how many packets are waiting to be sent
        public int WaitSnd => snd_buf.Count + snd_queue.Count;

        // ikcp_wnd_unused
        // returns the remaining space in receive window (rcv_wnd - rcv_queue)
        internal uint WndUnused()
        {
            if (rcv_queue.Count < rcv_wnd)
                return rcv_wnd - (uint)rcv_queue.Count;
            return 0;
        }

        // ikcp_recv
        // receive data from kcp state machine
        //   returns number of bytes read.
        //   returns negative on error.
        // note: pass negative length to peek.
        public int Receive(byte[] buffer, int len)
        {
            // kcp's ispeek feature is not supported.
            // this makes 'merge fragment' code significantly easier because
            // we can iterate while queue.Count > 0 and dequeue each time.
            // if we had to consider ispeek then count would always be > 0 and
            // we would have to remove only after the loop.
            //
            //bool ispeek = len < 0;
            if (len < 0)
                throw new NotSupportedException("Receive ispeek for negative len is not supported!");

            if (rcv_queue.Count == 0)
                return -1;

            if (len < 0) len = -len;

            int peeksize = PeekSize();

            if (peeksize < 0)
                return -2;

            if (peeksize > len)
                return -3;

            bool recover = rcv_queue.Count >= rcv_wnd;

            // merge fragment.
            int offset = 0;
            len = 0;
            // original KCP iterates rcv_queue and deletes if !ispeek.
            // removing from a c# queue while iterating is not possible, but
            // we can change to 'while Count > 0' and remove every time.
            // (we can remove every time because we removed ispeek support!)
            while (rcv_queue.Count > 0)
            {
                // unlike original kcp, we dequeue instead of just getting the
                // entry. this is fine because we remove it in ANY case.
                Segment seg = rcv_queue.Dequeue();

                // copy segment data into our buffer
                Buffer.BlockCopy(seg.data.GetBuffer(), 0, buffer, offset, (int)seg.data.Position);
                offset += (int)seg.data.Position;

                len += (int)seg.data.Position;
                uint fragment = seg.frg;

                // note: ispeek is not supported in order to simplify this loop

                // unlike original kcp, we don't need to remove seg from queue
                // because we already dequeued it.
                // simply delete it
                SegmentDelete(seg);

                if (fragment == 0)
                    break;
            }

            // move available data from rcv_buf -> rcv_queue
            int removed = 0;
            foreach (Segment seg in rcv_buf)
            {
                if (seg.sn == rcv_nxt && rcv_queue.Count < rcv_wnd)
                {
                    // can't remove while iterating. remember how many to remove
                    // and do it after the loop.
                    // note: don't return segment. we only add it to rcv_queue
                    ++removed;
                    // add
                    rcv_queue.Enqueue(seg);
                    // increase sequence number for next segment
                    rcv_nxt++;
                }
                else
                {
                    break;
                }
            }
            rcv_buf.RemoveRange(0, removed);

            // fast recover
            if (rcv_queue.Count < rcv_wnd && recover)
            {
                // ready to send back CMD_WINS in flush
                // tell remote my window size
                probe |= ASK_TELL;
            }

            return len;
        }

        // ikcp_peeksize
        // check the size of next message in the recv queue.
        // returns -1 if there is no message, or if the message is still incomplete.
        public int PeekSize()
        {
            int length = 0;

            // empty queue?
            if (rcv_queue.Count == 0) return -1;

            // peek the first segment
            Segment seq = rcv_queue.Peek();

            // seg.frg is 0 if the message requires no fragmentation.
            // in that case, the segment's size is the final message size.
            if (seq.frg == 0) return (int)seq.data.Position;

            // check if all fragment parts were received yet.
            // seg.frg is the n-th fragment, but in reverse.
            // this way the first received segment tells us how many fragments there are for the message.
            // for example, if a message contains 3 segments:
            //   first segment:  .frg is 2 (index in reverse)
            //   second segment: .frg is 1 (index in reverse)
            //   third segment:  .frg is 0 (index in reverse)
            if (rcv_queue.Count < seq.frg + 1) return -1;

            // recv_queue contains all the fragments necessary to reconstruct the message.
            // sum all fragment's sizes to get the full message size.
            foreach (Segment seg in rcv_queue)
            {
                length += (int)seg.data.Position;
                if (seg.frg == 0) break;
            }

            return length;
        }

        // ikcp_send
        // splits message into MTU sized fragments, adds them to snd_queue.
        public int Send(byte[] buffer, int offset, int len)
        {
            // fragment count
            int count;

            if (len < 0) return -1;

            // streaming mode: removed. we never want to send 'hello' and
            // receive 'he' 'll' 'o'. we want to always receive 'hello'.

            // calculate amount of fragments necessary for 'len'
            if (len <= mss) count = 1;
            else count = (int)((len + mss - 1) / mss);

            // IMPORTANT kcp encodes 'frg' as 1 byte.
            // so we can only support up to 255 fragments.
            // (which limits max message size to around 288 KB)
            // this is difficult to debug. let's make this 100% obvious.
            if (count > FRG_MAX)
                throw new Exception($"Send len={len} requires {count} fragments, but kcp can only handle up to {FRG_MAX} fragments.");

            // original kcp uses WND_RCV const instead of rcv_wnd runtime:
            // https://github.com/skywind3000/kcp/pull/291/files
            // which always limits max message size to 144 KB:
            //if (count >= WND_RCV) return -2;
            // using configured rcv_wnd uncorks max message size to 'any':
            if (count >= rcv_wnd) return -2;

            if (count == 0) count = 1;

            // fragment
            for (int i = 0; i < count; i++)
            {
                int size = len > (int)mss ? (int)mss : len;
                Segment seg = SegmentNew();

                if (len > 0)
                {
                    seg.data.Write(buffer, offset, size);
                }
                // seg.len = size: WriteBytes sets segment.Position!

                // set fragment number.
                // if the message requires no fragmentation, then
                // seg.frg becomes 1-0-1 = 0
                seg.frg = (uint)(count - i - 1);
                snd_queue.Enqueue(seg);
                offset += size;
                len -= size;
            }

            return 0;
        }

        // ikcp_update_ack
        void UpdateAck(int rtt) // round trip time
        {
            // https://tools.ietf.org/html/rfc6298
            if (rx_srtt == 0)
            {
                rx_srtt = rtt;
                rx_rttval = rtt / 2;
            }
            else
            {
                int delta = rtt - rx_srtt;
                if (delta < 0) delta = -delta;
                rx_rttval = (3 * rx_rttval + delta) / 4;
                rx_srtt   = (7 * rx_srtt + rtt) / 8;
                if (rx_srtt < 1) rx_srtt = 1;
            }
            int rto = rx_srtt + Math.Max((int)interval, 4 * rx_rttval);
            rx_rto = Utils.Clamp(rto, rx_minrto, RTO_MAX);
        }

        // ikcp_shrink_buf
        internal void ShrinkBuf()
        {
            if (snd_buf.Count > 0)
            {
                Segment seg = snd_buf[0];
                snd_una = seg.sn;
            }
            else
            {
                snd_una = snd_nxt;
            }
        }

        // ikcp_parse_ack
        // removes the segment with 'sn' from send buffer
        internal void ParseAck(uint sn)
        {
            if (Utils.TimeDiff(sn, snd_una) < 0 || Utils.TimeDiff(sn, snd_nxt) >= 0)
                return;

            // for-int so we can erase while iterating
            for (int i = 0; i < snd_buf.Count; ++i)
            {
                // is this the segment?
                Segment seg = snd_buf[i];
                if (sn == seg.sn)
                {
                    // remove and return
                    snd_buf.RemoveAt(i);
                    SegmentDelete(seg);
                    break;
                }
                if (Utils.TimeDiff(sn, seg.sn) < 0)
                {
                    break;
                }
            }
        }

        // ikcp_parse_una
        // removes all unacknowledged segments with sequence numbers < una from send buffer
        internal void ParseUna(uint una)
        {
            int removed = 0;
            foreach (Segment seg in snd_buf)
            {
                // if (Utils.TimeDiff(una, seg.sn) > 0)
                if (seg.sn < una)
                {
                    // can't remove while iterating. remember how many to remove
                    // and do it after the loop.
                    ++removed;
                    SegmentDelete(seg);
                }
                else
                {
                    break;
                }
            }
            snd_buf.RemoveRange(0, removed);
        }

        // ikcp_parse_fastack
        internal void ParseFastack(uint sn, uint ts) // serial number, timestamp
        {
            // sn needs to be between snd_una and snd_nxt
            // if !(snd_una <= sn && sn < snd_nxt) return;

            // if (Utils.TimeDiff(sn, snd_una) < 0)
            if (sn < snd_una)
                return;

            // if (Utils.TimeDiff(sn, snd_nxt) >= 0)
            if (sn >= snd_nxt)
                return;

            foreach (Segment seg in snd_buf)
            {
                // if (Utils.TimeDiff(sn, seg.sn) < 0)
                if (sn < seg.sn)
                {
                    break;
                }
                else if (sn != seg.sn)
                {
#if !FASTACK_CONSERVE
                    seg.fastack++;
#else
                    if (Utils.TimeDiff(ts, seg.ts) >= 0)
                        seg.fastack++;
#endif
                }
            }
        }

        // ikcp_ack_push
        // appends an ack.
        void AckPush(uint sn, uint ts) // serial number, timestamp
        {
            acklist.Add(new AckItem{ serialNumber = sn, timestamp = ts });
        }

        // ikcp_parse_data
        void ParseData(Segment newseg)
        {
            uint sn = newseg.sn;

            if (Utils.TimeDiff(sn, rcv_nxt + rcv_wnd) >= 0 ||
                Utils.TimeDiff(sn, rcv_nxt) < 0)
            {
                SegmentDelete(newseg);
                return;
            }

            InsertSegmentInReceiveBuffer(newseg);
            MoveReceiveBufferReadySegmentsToQueue();
        }

        // inserts the segment into rcv_buf, ordered by seg.sn.
        // drops the segment if one with the same seg.sn already exists.
        // goes through receive buffer in reverse order for performance.
        //
        // note: see KcpTests.InsertSegmentInReceiveBuffer test!
        // note: 'insert or delete' can be done in different ways, but let's
        //       keep consistency with original C kcp.
        internal void InsertSegmentInReceiveBuffer(Segment newseg)
        {
            bool repeat = false; // 'duplicate'

            // original C iterates backwards, so we need to do that as well.
            // note if rcv_buf.Count == 0, i becomes -1 and no looping happens.
            int i;
            for (i = rcv_buf.Count - 1; i >= 0; i--)
            {
                Segment seg = rcv_buf[i];
                if (seg.sn == newseg.sn)
                {
                    // duplicate segment found. nothing will be added.
                    repeat = true;
                    break;
                }
                if (Utils.TimeDiff(newseg.sn, seg.sn) > 0)
                {
                    // this entry's sn is < newseg.sn, so let's stop
                    break;
                }
            }

            // no duplicate? then insert.
            if (!repeat)
            {
                rcv_buf.Insert(i + 1, newseg);
            }
            // duplicate. just delete it.
            else
            {
                SegmentDelete(newseg);
            }
        }

        // move ready segments from rcv_buf -> rcv_queue.
        // moves only the ready segments which are in rcv_nxt sequence order.
        // some may still be missing an inserted later.
        void MoveReceiveBufferReadySegmentsToQueue()
        {
            int removed = 0;
            foreach (Segment seg in rcv_buf)
            {
                // move segments while they are in 'rcv_nxt' sequence order.
                // some may still be missing and inserted later, in this case it stops immediately
                // because segments always need to be received in the exact sequence order.
                if (seg.sn == rcv_nxt && rcv_queue.Count < rcv_wnd)
                {
                    // can't remove while iterating. remember how many to remove
                    // and do it after the loop.
                    ++removed;
                    rcv_queue.Enqueue(seg);
                    // increase sequence number for next segment
                    rcv_nxt++;
                }
                else
                {
                    break;
                }
            }
            rcv_buf.RemoveRange(0, removed);
        }

        // ikcp_input
        // used when you receive a low level packet (e.g. UDP packet)
        // => original kcp uses offset=0, we made it a parameter so that high
        //    level can skip the channel byte more easily
        public int Input(byte[] data, int offset, int size)
        {
            uint prev_una = snd_una;
            uint maxack = 0;
            uint latest_ts = 0;
            int flag = 0;

            if (data == null || size < OVERHEAD) return -1;

            while (true)
            {
                // enough data left to decode segment (aka OVERHEAD bytes)?
                if (size < OVERHEAD) break;

                // decode segment
                offset += Utils.Decode32U(data, offset, out uint conv_);
                if (conv_ != conv) return -1;

                offset += Utils.Decode8u(data, offset, out byte cmd);
                // IMPORTANT kcp encodes 'frg' as 1 byte.
                // so we can only support up to 255 fragments.
                // (which limits max message size to around 288 KB)
                offset += Utils.Decode8u(data, offset, out byte frg);
                offset += Utils.Decode16U(data, offset, out ushort wnd);
                offset += Utils.Decode32U(data, offset, out uint ts);
                offset += Utils.Decode32U(data, offset, out uint sn);
                offset += Utils.Decode32U(data, offset, out uint una);
                offset += Utils.Decode32U(data, offset, out uint len);

                // reduce remaining size by what was read
                size -= OVERHEAD;

                // enough remaining to read 'len' bytes of the actual payload?
                // note: original kcp casts uint len to int for <0 check.
                if (size < len || (int)len < 0) return -2;

                // validate command type
                if (cmd != CMD_PUSH && cmd != CMD_ACK &&
                    cmd != CMD_WASK && cmd != CMD_WINS)
                    return -3;

                rmt_wnd = wnd;
                ParseUna(una);
                ShrinkBuf();

                if (cmd == CMD_ACK)
                {
                    if (Utils.TimeDiff(current, ts) >= 0)
                    {
                        UpdateAck(Utils.TimeDiff(current, ts));
                    }
                    ParseAck(sn);
                    ShrinkBuf();
                    if (flag == 0)
                    {
                        flag = 1;
                        maxack = sn;
                        latest_ts = ts;
                    }
                    else
                    {
                        if (Utils.TimeDiff(sn, maxack) > 0)
                        {
#if !FASTACK_CONSERVE
                            maxack = sn;
                            latest_ts = ts;
#else
                            if (Utils.TimeDiff(ts, latest_ts) > 0)
                            {
                                maxack = sn;
                                latest_ts = ts;
                            }
#endif
                        }
                    }
                }
                else if (cmd == CMD_PUSH)
                {
                    if (Utils.TimeDiff(sn, rcv_nxt + rcv_wnd) < 0)
                    {
                        AckPush(sn, ts);
                        if (Utils.TimeDiff(sn, rcv_nxt) >= 0)
                        {
                            Segment seg = SegmentNew();
                            seg.conv = conv_;
                            seg.cmd = cmd;
                            seg.frg = frg;
                            seg.wnd = wnd;
                            seg.ts  = ts;
                            seg.sn  = sn;
                            seg.una = una;
                            if (len > 0)
                            {
                                seg.data.Write(data, offset, (int)len);
                            }
                            ParseData(seg);
                        }
                    }
                }
                else if (cmd == CMD_WASK)
                {
                    // ready to send back CMD_WINS in flush
                    // tell remote my window size
                    probe |= ASK_TELL;
                }
                else if (cmd == CMD_WINS)
                {
                    // do nothing
                }
                else
                {
                    return -3;
                }

                offset += (int)len;
                size -= (int)len;
            }

            if (flag != 0)
            {
                ParseFastack(maxack, latest_ts);
            }

            // cwnd update when packet arrived
            if (Utils.TimeDiff(snd_una, prev_una) > 0)
            {
                if (cwnd < rmt_wnd)
                {
                    if (cwnd < ssthresh)
                    {
                        cwnd++;
                        incr += mss;
                    }
                    else
                    {
                        if (incr < mss) incr = mss;
                        incr += (mss * mss) / incr + (mss / 16);
                        if ((cwnd + 1) * mss <= incr)
                        {
                            cwnd = (incr + mss - 1) / ((mss > 0) ? mss : 1);
                        }
                    }
                    if (cwnd > rmt_wnd)
                    {
                        cwnd = rmt_wnd;
                        incr = rmt_wnd * mss;
                    }
                }
            }

            return 0;
        }

        // flush helper function
        void MakeSpace(ref int size, int space)
        {
            if (size + space > mtu)
            {
                output(buffer, size);
                size = 0;
            }
        }

        // flush helper function
        void FlushBuffer(int size)
        {
            // flush buffer up to 'offset' (<= MTU)
            if (size > 0)
            {
                output(buffer, size);
            }
        }

        // ikcp_flush
        // flush remain ack segments.
        // flush may output multiple <= MTU messages from MakeSpace / FlushBuffer.
        // the amount of messages depends on the sliding window.
        // configured by send/receive window sizes + congestion control.
        // with congestion control, the window will be extremely small(!).
        public void Flush()
        {
            int size  = 0;     // amount of bytes to flush. 'buffer ptr' in C.
            bool lost = false; // lost segments

            // update needs to be called before flushing
            if (!updated) return;

            // kcp only stack allocates a segment here for performance, leaving
            // its data buffer null because this segment's data buffer is never
            // used. that's fine in C, but in C# our segment is a class so we
            // need to allocate and most importantly, not forget to deallocate
            // it before returning.
            Segment seg = SegmentNew();
            seg.conv = conv;
            seg.cmd = CMD_ACK;
            seg.wnd = WndUnused();
            seg.una = rcv_nxt;

            // flush acknowledges
            foreach (AckItem ack in acklist)
            {
                MakeSpace(ref size, OVERHEAD);
                // ikcp_ack_get assigns ack[i] to seg.sn, seg.ts
                seg.sn = ack.serialNumber;
                seg.ts = ack.timestamp;
                size += seg.Encode(buffer, size);
            }
            acklist.Clear();

            // probe window size (if remote window size equals zero)
            if (rmt_wnd == 0)
            {
                if (probe_wait == 0)
                {
                    probe_wait = PROBE_INIT;
                    ts_probe = current + probe_wait;
                }
                else
                {
                    if (Utils.TimeDiff(current, ts_probe) >= 0)
                    {
                        if (probe_wait < PROBE_INIT)
                            probe_wait = PROBE_INIT;
                        probe_wait += probe_wait / 2;
                        if (probe_wait > PROBE_LIMIT)
                            probe_wait = PROBE_LIMIT;
                        ts_probe = current + probe_wait;
                        probe |= ASK_SEND;
                    }
                }
            }
            else
            {
                ts_probe = 0;
                probe_wait = 0;
            }

            // flush window probing commands
            if ((probe & ASK_SEND) != 0)
            {
                seg.cmd = CMD_WASK;
                MakeSpace(ref size, OVERHEAD);
                size += seg.Encode(buffer, size);
            }

            // flush window probing commands
            if ((probe & ASK_TELL) != 0)
            {
                seg.cmd = CMD_WINS;
                MakeSpace(ref size, OVERHEAD);
                size += seg.Encode(buffer, size);
            }

            probe = 0;

            // calculate the window size which is currently safe to send.
            // it's send window, or remote window, whatever is smaller.
            // for our max
            uint cwnd_ = Math.Min(snd_wnd, rmt_wnd);

            // double negative: if congestion window is enabled:
            // limit window size to cwnd.
            //
            // note this may heavily limit window sizes.
            // for our max message size test with super large windows of 32k,
            // 'congestion window' limits it down from 32.000 to 2.
            if (!nocwnd) cwnd_ = Math.Min(cwnd, cwnd_);

            // move cwnd_ 'window size' messages from snd_queue to snd_buf
            //   'snd_nxt' is what we want to send.
            //   'snd_una' is what hasn't been acked yet.
            //   copy up to 'cwnd_' difference between them (sliding window)
            while (Utils.TimeDiff(snd_nxt, snd_una + cwnd_) < 0)
            {
                if (snd_queue.Count == 0) break;

                Segment newseg = snd_queue.Dequeue();

                newseg.conv = conv;
                newseg.cmd = CMD_PUSH;
                newseg.wnd = seg.wnd;
                newseg.ts = current;
                newseg.sn = snd_nxt;
                snd_nxt += 1; // increase sequence number for next segment
                newseg.una = rcv_nxt;
                newseg.resendts = current;
                newseg.rto = rx_rto;
                newseg.fastack = 0;
                newseg.xmit = 0;
                snd_buf.Add(newseg);
            }

            // calculate resent
            uint resent = fastresend > 0 ? (uint)fastresend : 0xffffffff;
            uint rtomin = nodelay == 0 ? (uint)rx_rto >> 3 : 0;

            // flush data segments
            int change = 0;
            foreach (Segment segment in snd_buf)
            {
                bool needsend = false;

                // initial transmit
                if (segment.xmit == 0)
                {
                    needsend = true;
                    segment.xmit++;
                    segment.rto = rx_rto;
                    segment.resendts = current + (uint)segment.rto + rtomin;
                }
                // RTO
                else if (Utils.TimeDiff(current, segment.resendts) >= 0)
                {
                    needsend = true;
                    segment.xmit++;
                    xmit++;
                    if (nodelay == 0)
                    {
                        segment.rto += Math.Max(segment.rto, rx_rto);
                    }
                    else
                    {
                        int step = (nodelay < 2) ? segment.rto : rx_rto;
                        segment.rto += step / 2;
                    }
                    segment.resendts = current + (uint)segment.rto;
                    lost = true;
                }
                // fast retransmit
                else if (segment.fastack >= resent)
                {
                    if (segment.xmit <= fastlimit || fastlimit <= 0)
                    {
                        needsend = true;
                        segment.xmit++;
                        segment.fastack = 0;
                        segment.resendts = current + (uint)segment.rto;
                        change++;
                    }
                }

                if (needsend)
                {
                    segment.ts = current;
                    segment.wnd = seg.wnd;
                    segment.una = rcv_nxt;

                    int need = OVERHEAD + (int)segment.data.Position;
                    MakeSpace(ref size, need);

                    size += segment.Encode(buffer, size);

                    if (segment.data.Position > 0)
                    {
                        Buffer.BlockCopy(segment.data.GetBuffer(), 0, buffer, size, (int)segment.data.Position);
                        size += (int)segment.data.Position;
                    }

                    // dead link happens if a message was resent N times, but an
                    // ack was still not received.
                    if (segment.xmit >= dead_link)
                    {
                        state = -1;
                    }
                }
            }

            // kcp stackallocs 'seg'. our C# segment is a class though, so we
            // need to properly delete and return it to the pool now that we are
            // done with it.
            SegmentDelete(seg);

            // flush remaining segments
            FlushBuffer(size);

            // update ssthresh
            // rate halving, https://tools.ietf.org/html/rfc6937
            if (change > 0)
            {
                uint inflight = snd_nxt - snd_una;
                ssthresh = inflight / 2;
                if (ssthresh < THRESH_MIN)
                    ssthresh = THRESH_MIN;
                cwnd = ssthresh + resent;
                incr = cwnd * mss;
            }

            // congestion control, https://tools.ietf.org/html/rfc5681
            if (lost)
            {
                // original C uses 'cwnd', not kcp->cwnd!
                ssthresh = cwnd_ / 2;
                if (ssthresh < THRESH_MIN)
                    ssthresh = THRESH_MIN;
                cwnd = 1;
                incr = mss;
            }

            if (cwnd < 1)
            {
                cwnd = 1;
                incr = mss;
            }
        }

        // ikcp_update
        // update state (call it repeatedly, every 10ms-100ms), or you can ask
        // Check() when to call it again (without Input/Send calling).
        //
        // 'current' - current timestamp in millisec. pass it to Kcp so that
        // Kcp doesn't have to do any stopwatch/deltaTime/etc. code
        //
        // time as uint, likely to minimize bandwidth.
        // uint.max = 4294967295 ms = 1193 hours = 49 days
        public void Update(uint currentTimeMilliSeconds)
        {
            current = currentTimeMilliSeconds;

            // not updated yet? then set updated and last flush time.
            if (!updated)
            {
                updated = true;
                ts_flush = current;
            }

            // slap is time since last flush in milliseconds
            int slap = Utils.TimeDiff(current, ts_flush);

            // hard limit: if 10s elapsed, always flush no matter what
            if (slap >= 10000 || slap < -10000)
            {
                ts_flush = current;
                slap = 0;
            }

            // last flush is increased by 'interval' each time.
            // so slap >= is a strange way to check if interval has elapsed yet.
            if (slap >= 0)
            {
                // increase last flush time by one interval
                ts_flush += interval;

                // if last flush is still behind, increase it to current + interval
                // if (Utils.TimeDiff(current, ts_flush) >= 0) // original kcp.c
                if (current >= ts_flush)                       // less confusing
                {
                    ts_flush = current + interval;
                }
                Flush();
            }
        }

        // ikcp_check
        // Determine when should you invoke update
        // Returns when you should invoke update in millisec, if there is no
        // input/send calling. you can call update in that time, instead of
        // call update repeatly.
        //
        // Important to reduce unnecessary update invoking. use it to schedule
        // update (e.g. implementing an epoll-like mechanism, or optimize update
        // when handling massive kcp connections).
        public uint Check(uint current_)
        {
            uint ts_flush_ = ts_flush;
            // int tm_flush = 0x7fffffff; original kcp: useless assignment
            int tm_packet = 0x7fffffff;

            if (!updated)
            {
                return current_;
            }

            if (Utils.TimeDiff(current_, ts_flush_) >= 10000 ||
                Utils.TimeDiff(current_, ts_flush_) < -10000)
            {
                ts_flush_ = current_;
            }

            if (Utils.TimeDiff(current_, ts_flush_) >= 0)
            {
                return current_;
            }

            int tm_flush = Utils.TimeDiff(ts_flush_, current_);

            foreach (Segment seg in snd_buf)
            {
                int diff = Utils.TimeDiff(seg.resendts, current_);
                if (diff <= 0)
                {
                    return current_;
                }
                if (diff < tm_packet) tm_packet = diff;
            }

            uint minimal = (uint)(tm_packet < tm_flush ? tm_packet : tm_flush);
            if (minimal >= interval) minimal = interval;

            return current_ + minimal;
        }

        // ikcp_setmtu
        // Change MTU (Maximum Transmission Unit) size.
        public void SetMtu(uint mtu)
        {
            if (mtu < 50 || mtu < OVERHEAD)
                throw new ArgumentException("MTU must be higher than 50 and higher than OVERHEAD");

            buffer = new byte[(mtu + OVERHEAD) * 3];
            this.mtu = mtu;
            mss = mtu - OVERHEAD;
        }

        // ikcp_interval
        public void SetInterval(uint interval)
        {
            // clamp interval between 10 and 5000
            if      (interval > 5000) interval = 5000;
            else if (interval < 10)   interval = 10;
            this.interval = interval;
        }

        // ikcp_nodelay
        // configuration: https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration
        //   nodelay : Whether nodelay mode is enabled, 0 is not enabled; 1 enabled.
        //   interval ：Protocol internal work interval, in milliseconds, such as 10 ms or 20 ms.
        //   resend ：Fast retransmission mode, 0 represents off by default, 2 can be set (2 ACK spans will result in direct retransmission)
        //   nc ：Whether to turn off flow control, 0 represents “Do not turn off” by default, 1 represents “Turn off”.
        // Normal Mode: ikcp_nodelay(kcp, 0, 40, 0, 0);
        // Turbo Mode： ikcp_nodelay(kcp, 1, 10, 2, 1);
        public void SetNoDelay(uint nodelay, uint interval = INTERVAL, int resend = 0, bool nocwnd = false)
        {
            this.nodelay = nodelay;
            if (nodelay != 0)
            {
                rx_minrto = RTO_NDL;
            }
            else
            {
                rx_minrto = RTO_MIN;
            }

            if (interval >= 0)
            {
                // clamp interval between 10 and 5000
                if (interval > 5000) interval = 5000;
                else if (interval < 10) interval = 10;
                this.interval = interval;
            }

            if (resend >= 0)
            {
                fastresend = resend;
            }

            this.nocwnd = nocwnd;
        }

        // ikcp_wndsize
        public void SetWindowSize(uint sendWindow, uint receiveWindow)
        {
            if (sendWindow > 0)
            {
                snd_wnd = sendWindow;
            }

            if (receiveWindow > 0)
            {
                // must >= max fragment size
                rcv_wnd = Math.Max(receiveWindow, WND_RCV);
            }
        }
    }
}
