using Newtonsoft.Json.Linq;
using System;
using System.Numerics;
using System.Text;
using TMPro;
using UdonSharp;
using UnityEngine;
using VRC.SDK3.UdonNetworkCalling;
using VRC.SDKBase;
using VRC.Udon.Common;

namespace Marro.PacManUdon
{
    public enum NetworkEventType
    {
        FullSync = 1,
        InputChange = 2,
        StartGameButtonPressed = 3,
        Pause = 7,
        Resume = 8,
        Step = 9,
    }

    public class NetworkManager : SyncedObject
    {
        // The network manager works by serializing event and state data into a byte array, including a timestamp for each event.
        // If user is owner, this data is created and stored in a buffer which is cleared upon transmission.
        // If user is not owner, this data is read into the same buffer and replayed based on the included timestamp.
        // Data replay is delayed with an offset on the timestamp to hide inconsistency in latency.

        // Each event contains a header. The first part contains the size of the event as a ushort, allowing a maximum size of 65,536 bytes.
        // After that timestamp is transferred in seconds since the owner started the game as a float, matching Unity's time representation.
        // Afterwards is a message id, a single byte which increments (with overflow) for every event to confirm data completeness.
        // Lastly, a single byte indicates the type of event. 0 is a full sync, which is reserved for syncing up from any unknown state.

        // A byte array is used as a DataList or DataDictionary can only be transmitted as JSON which is much less efficient.
        // As Udon does not support instantiating objects, I have not created classes to represent the data being sent.
        // Correct parsing is dependend upon everything being read out identially to how it was created.

        // An event has the following structure:
        // [0-1]: (ushort) Size of event.
        // [2-5]: (float) Time in seconds at which event occured.
        // [6]: (byte) Message id, increments by one for each message with the same timestamp.
        // [7]: (byte) Type of event. 0 = Full Sync, which is used to sync up from an undefinted state.
        // [+]: Event-specific data.

        #region Settings
        /// <summary>
        /// The root from which this <see cref="NetworkManager"/> will look for <see cref="SyncedObject"/> to control.
        /// </summary>
        [SerializeField] private GameObject root;
        /// <summary>
        /// The delay in ticks at which the receiving side replays events.
        /// </summary>
        [SerializeField] private int delay = 1;
        /// <summary>
        /// The maximum amount of times a message is sent.
        /// </summary>
        [SerializeField] private int maxEventSendTries = 3;
        /// <summary>
        /// How long to wait since last message to send next message.
        /// </summary>
        [SerializeField] private int sendingPingDelay = 15;
        /// <summary>
        /// How long to wait since last attempt to request sync.
        /// </summary>
        [SerializeField] private int syncingPingDelay = 300;
        /// <summary>
        /// The time delta at which updates occur.
        /// </summary>
        [SerializeField] private float tickDelta = 0.0165f;
        #endregion

        #region Constants
        /// <summary>
        /// The maximum amount of events in the buffer.
        /// </summary>
        private const int BufferMaxTotalEvents = 255;
        /// <summary>
        /// The maximum amount of events in the buffer.
        /// </summary>
        private const int MaxEventSize = ushort.MaxValue;

        /// <summary>
        /// The index in an event where the event size is stored.
        /// </summary>
        private const ushort HeaderEventSizeIndex = 0;
        /// <summary>
        /// The index in an event where the timestamp is stored.
        /// </summary>
        private const ushort HeaderTimestampIndex = 2;
        /// <summary>
        /// The index in an event where the event id is stored.
        /// </summary>
        private const ushort HeaderEventIdIndex = 6;
        /// <summary>
        /// The index in an event where the event type is stored.
        /// </summary>
        private const ushort HeaderEventTypeIndex = 7;
        /// <summary>
        /// The total length of the header of an event, in bytes.
        /// </summary>
        private const ushort HeaderLength = 8;

        private const ushort GuidSize = 16;
        private const ushort ResetEventSize = HeaderTimestampIndex + GuidSize;
        #endregion

        #region Private attributes
        /// <summary>
        /// Whether <see cref="Initialize"/> has been called successfully.
        /// </summary>
        private bool initialized = false;

        /// <summary>
        /// Subscribers to each <see cref="NetworkEventType"/>.
        /// </summary>
        private SyncedObject[][] networkEventSubscribers;

        /// <summary>
        /// Indices for <see cref="networkEventSubscribers"/>.
        /// </summary>
        private int[] networkEventSubscribersIndices;

        /// <summary>
        /// Subscribers for <see cref="SyncedObject.SyncedUpdate"/>.
        /// </summary>
        private SyncedObject[] syncedUpdateSubscribers;

        /// <summary>
        /// The <see cref="Time.fixedTime"/> that corresponds to tick 0.
        /// </summary>
        private float startTime;

        /// <summary>
        /// Time in ticks since start of game.
        /// </summary>
        private int targetTicks;

        /// <summary>
        /// True if time is paused
        /// </summary>
        private bool paused;

        /// <summary>
        /// True if a step should happen next update
        /// </summary>
        private bool stepNext;

        /// <summary>
        /// Time at which next received event occured, in ticks.
        /// </summary>
        private int nextEventTime;

        /// <summary>
        /// Amounot of retries in a row without a successful sync.
        /// </summary>
        private int retriesWithoutSuccess;

        private byte[] lastResetEventData;

        /// <summary>
        /// For receiver: True if there's a full sync in the queue and we are not synced, otherwise false.
        /// For transmitter: True if there's a full sync in the queue which has not yet been transmitted, otherwise false.
        /// </summary>
        private bool hasFullSyncReady;
        /// <summary>
        /// True if serialization has been requestsed
        /// </summary>
        private bool serializationRequested;

        /// <summary>
        /// Events to send at the end of SyncedUpdate cycle.
        /// </summary>
        private NetworkEventType[] eventsToSendEarly;
        /// <summary>
        /// Index for <see cref="eventsToSendEarly"/>.
        /// </summary>
        private int eventsToSendEarlyIndex;
        /// <summary>
        /// Events to send at the end of SyncedUpdate cycle, delayed by a tick.
        /// </summary>
        private NetworkEventType[] eventsToSendLate;
        /// <summary>
        /// Index for <see cref="eventsToSendLate"/>.
        /// </summary>
        private int eventsToSendLateIndex;

        /// <summary>
        /// Queue of events to be transmitted or processed.
        /// </summary>
        private byte[][] eventsQueue;
        /// <summary>
        /// Index of <see cref="eventsQueue"/>.
        /// </summary>
        private int eventsQueueIndex;

        /// <summary>
        /// The value of <see cref="eventsQueueIndex"/> at the last transmission.
        /// </summary>
        private int eventsQueueIndexAtLastTransmission;
        /// <summary>
        /// Counts of new events at recent transmissions.
        /// </summary>
        private int[] eventTransmissionHistory;
        /// <summary>
        /// Index of <see cref="eventTransmissionHistoryIndex"/>.
        /// </summary>
        private int eventTransmissionHistoryIndex;
        /// <summary>
        /// Time of last event transmission sent, requested or received, in ticks.
        /// </summary>
        private int lastEventTransmissionTime;


        /// <summary>
        /// The message id of the most recent event created or received.
        /// </summary>
        private byte lastEventId;

        /// <summary>
        /// Data which is currently available on the network.
        /// </summary>
        [UdonSynced] private byte[] networkedData = new byte[0];
        #endregion

        #region Public fields
        private bool ready = false;
        /// <summary>
        /// Whether this <see cref="NetworkManager"/> is ready to transmit or receive data.
        /// If false, networking is disabled and this <see cref="NetworkManager"/> acts as a pass-through.
        /// </summary>
        public bool Ready
        {
            get => ready;
            private set
            {
                ready = value;
                if (DebugImageToIndicateReady != null)
                {
                    DebugImageToIndicateReady.SetFloat("Color", value ? 1 : 0);
                }
            }
        }


        /// <summary>
        /// Whether the current perspective is synced with the owner. (Always true if current perspective is owner.)
        /// </summary>
        public bool Synced
        {
            get => synced;
            private set
            {
                synced = value;
                if (DebugImageToIndicateSynced != null)
                {
                    DebugImageToIndicateSynced.SetFloat("Color", value ? 1 : 0);
                }
            }
        }
        private bool synced = false;

        /// <summary>
        /// The time since start of game, in ticks.
        /// </summary>
        public int SyncedTimeTicks { get; private set; }
        /// <summary>
        /// The time since start of game which is currently being simulated.
        /// </summary>
        public float SyncedTime { get; private set; }

        /// <summary>
        /// Time since the last simulation, in seconds.
        /// </summary>
        public float SyncedDeltaTime => tickDelta;

        /// <summary>
        /// Is the current simulation to prepare for applying a network event?
        /// True = Yes, This update is preparing for a network update.
        /// False = No, this update is after the network update or there was no network update this cycle.
        /// </summary>
        public bool IsEventUpdate { get; private set; }

        /// <summary>
        /// Is the local user owner?
        /// </summary>
        public bool IsOwner
        {
            get => isOwner;
            private set
            {
                isOwner = value;
                if (DebugImageToIndicateOwner != null)
                {
                    DebugImageToIndicateOwner.SetFloat("Color", value ? 1 : 0);
                }
            }
        }
        private bool isOwner = false;
        #endregion

        #region General
        /// <summary>
        /// Initializes the <see cref="NetworkManager"/>.
        /// </summary>
        public void Initialize()
        {
            if (!BitConverter.IsLittleEndian)
            {
                Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: NetworkManager only supports little endian! Network sync will not be possible.");
                return;
            }

            root = transform.parent.gameObject;

            InitializeSubscribers();

            initialized = true;

            Reset();
        }

        private void InitializeSubscribers()
        {
            syncedUpdateSubscribers = root.GetComponentsInChildren<SyncedObject>(includeInactive: true);

            foreach (var obj in syncedUpdateSubscribers)
            {
                obj.networkManager = this;
            }

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Found {syncedUpdateSubscribers.Length} {nameof(SyncedObject)} in children of {root.name}.");

            const int eventTypeCount = byte.MaxValue + 1;

            networkEventSubscribers = new SyncedObject[eventTypeCount][];
            networkEventSubscribersIndices = new int[eventTypeCount];

            lastResetEventData = new byte[ResetEventSize];
        }

        public void Reset()
        {
            if (!initialized)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Tried to call {nameof(Reset)} while not initialized. Call {nameof(Initialize)} first.");
                return;
            }

            if (tester == null)
            {
                IsOwner = Networking.IsOwner(gameObject);
            }
            else
            {
                IsOwner = tester.ShouldBeOwner(this);
            }

            ClearBuffer();

            retriesWithoutSuccess = 0;

            targetTicks = 0;
            startTime = Time.fixedTime;
            SyncedTime = 0;
            SyncedTimeTicks = 0;

            Ready = true;

            // Sync up
            if (!IsOwner)
            {
                SetSynced(false);
            }
            else
            {
                Synced = true; // Owner is always synced
                SendReset();
            }

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Initialized");
        }

        public void Update()
        {
            if (debugOutput != null)
            {
                WriteDebugOutput(debugOutput);
            }

            if (!initialized)
            {
                return;
            }

            // Get our target time
            UpdateInternalTime();

            // Forwards simulated time by updateDelta until we're caught up
            while (SyncedTimeTicks <= targetTicks)
            {
                PerformFixedSyncedUpdate();
            }
        }

        private void PerformFixedSyncedUpdate()
        {
            if (Ready)
            {
                if (isOwner)
                {
                    ProcessEventsToSend(); // Prepare events from last cycle
                    ProgressSendingPingTime(); // See if we need to send a ping
                }
                else if (Synced || hasFullSyncReady)
                {
                    ApplyReceivedEvents(); // See if there's events after last update that need to be replayed
                }
                else
                {
                    ProgressSyncingPingTime(); // See if we should request syncing
                }
            }

            ProgressSyncedTime();
            CallSyncedUpdate();
        }

        private void CallSyncedUpdate()
        {
            IsEventUpdate = false;

            for (int i = 0; i < syncedUpdateSubscribers.Length; i++)
            {
                var obj = syncedUpdateSubscribers[i];

                if (obj.gameObject.activeInHierarchy)
                {
                    obj.SyncedUpdate();
                }
            }
        }

        private void HandleError(bool clearBuffer)
        {
            retriesWithoutSuccess++;

            if (retriesWithoutSuccess > 3)
            {
                Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: Retried 3 times without success.");
                Ready = false;
                return;
            }

            Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Encountered data error, attempting to recover via full sync.");

            if (clearBuffer)
            {
                ClearBuffer();
            }

            if (!IsOwner)
            {
                SetSynced(false);
            }
            else if (clearBuffer)
            {
                SendReset();
            }
        }

        private SyncedObject[] GetEventSubscribers(NetworkEventType eventType) => networkEventSubscribers[(int)eventType];

        public void SubscribeToEvent(SyncedObject syncedObject, NetworkEventType eventType)
        {
            // This is inefficient, but I'd rather initialize slowly than perform bounds checks in often called code

            var eventTypeIndex = (int)eventType;

            var subscribers = networkEventSubscribers[eventTypeIndex];
            int subscribersIndex = networkEventSubscribersIndices[eventTypeIndex];
            if (subscribers == null)
            {
                subscribers = new SyncedObject[1];
            }
            else
            {
                subscribers = new SyncedObject[subscribersIndex+1];
                Array.Copy(networkEventSubscribers[eventTypeIndex], subscribers, subscribersIndex);
            }

            subscribers[subscribersIndex] = syncedObject;

            networkEventSubscribers[eventTypeIndex] = subscribers;
            networkEventSubscribersIndices[eventTypeIndex] = subscribersIndex;
        }
        #endregion

        #region Sender
        public void SendEventSoon(NetworkEventType eventType, bool early = false)
        {
            if (!Ready)
            {
                return;
            }

            if (!IsOwner)
            {
                //Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEventSoon)} while not the owner!");
                return;
            }

            if (early)
            {
                if (eventsToSendEarlyIndex >= eventsToSendEarly.Length)
                {
                    Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(eventsToSendEarly)} overflow!");
                    HandleError(true);
                    return;
                }

                eventsToSendEarly[eventsToSendEarlyIndex++] = eventType;
            }
            else
            {
                if (eventsToSendLateIndex >= eventsToSendLate.Length)
                {
                    Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(eventsToSendLate)} overflow!");
                    HandleError(true);
                    return;
                }

                eventsToSendLate[eventsToSendLateIndex++] = eventType;
            }

            if (eventType == NetworkEventType.FullSync)
            {
                hasFullSyncReady = true;
            }
        }

        private void ProcessEventsToSend()
        {
            for (int i = 0; i < eventsToSendEarlyIndex; i++)
            {
                SendEventNow(eventsToSendEarly[i], true);
            }

            eventsToSendEarlyIndex = 0;

            for (int i = 0; i < eventsToSendLateIndex; i++)
            {
                SendEventNow(eventsToSendLate[i], false);
            }

            eventsToSendLateIndex = 0;
        }

        public void SendEventNow(NetworkEventType eventType, bool early)
        {
            if (!Ready)
            {
                return;
            }

            if (!IsOwner)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEventNow)} while not the owner!");
                return;
            }

            var timestamp = SyncedTimeTicks;

            if (early)
            {
                timestamp--;
            }

            var eventId = GetNextEventId(lastEventId);

            InitializeEvent(eventType, timestamp, eventId, out byte[] data, out var index);

            var subscibers = GetEventSubscribers(eventType);

            if (subscibers != null)
            {
                foreach (var obj in subscibers)
                {
                    obj.CollectSyncedData(data, ref index, eventType);
                }
            }

            // Validate and fill in event size
            var eventSizeBytes = BitConverter.GetBytes((ushort)index);
            Array.Copy(eventSizeBytes, 0, data, HeaderEventSizeIndex, eventSizeBytes.Length);

            data = GetArrayPart(data, 0, index);

            QueueEventInBuffer(data);

            Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Prepared event of type {eventType} with {data.Length} bytes, timestamp {timestamp} and id {eventId} for serialization.");

            RequestSerializationForEvents();

            lastEventId = eventId;

            retriesWithoutSuccess = 0; // We had success!
        }

        private static void InitializeEvent(NetworkEventType eventType, int timestamp, byte eventId, out byte[] data, out int index)
        {
            data = new byte[MaxEventSize];

            // Create header (note: event size is added later)
            var timestampBytes = BitConverter.GetBytes(timestamp);
            Array.Copy(timestampBytes, 0, data, HeaderTimestampIndex, timestampBytes.Length);
            data[HeaderEventIdIndex] = eventId;
            data[HeaderEventTypeIndex] = ((int)eventType).ToByte();
            index = HeaderLength;
        }

        private void SendReset()
        {
            var data = new byte[ResetEventSize];

            var guid = Guid.NewGuid().ToByteArray();
            Array.Copy(guid, 0, data, HeaderTimestampIndex, GuidSize);

            Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Sent Reset event!");

            QueueEventInBuffer(data);
            RequestSerializationForEvents();
        }

        private void RequestSerializationForEvents()
        {
            RequestSerialization();
            serializationRequested = true;
            if (tester != null)
            {
                tester.RequestSerializationTest();
            }
        }

        [NetworkCallable]
        public void RequestEventReceived(NetworkEventType eventType)
        {
            if (!Ready)
            {
                return;
            }

            if (eventType == NetworkEventType.FullSync && hasFullSyncReady)
            {
                //Debug.Log($"Rejected event request because already have full sync ready");
                return; // Don't send another full sync if we're already preparing to send one
            }

            SendEventSoon(eventType);
        }

        private void ProgressSendingPingTime()
        {
            if (eventsQueueIndex > 0 && !serializationRequested
                && targetTicks - lastEventTransmissionTime >= sendingPingDelay)
            {
                RequestSerializationForEvents();
            }
        }

        private void ProgressSyncingPingTime()
        {
            if (targetTicks - lastEventTransmissionTime >= syncingPingDelay)
            {
                RequestEvent(NetworkEventType.FullSync);
            }
        }

        public override void OnPreSerialization()
        {
            if (!Ready || !IsOwner || !serializationRequested || eventsQueue == null || eventsQueueIndex == 0)
            {
                return;
            }

            networkedData = Flatten(eventsQueue, 0, eventsQueueIndex);

            eventTransmissionHistory[eventTransmissionHistoryIndex] = eventsQueueIndex - eventsQueueIndexAtLastTransmission;
            eventTransmissionHistoryIndex += 1;
            if (eventTransmissionHistoryIndex >= eventTransmissionHistory.Length)
            {
                eventTransmissionHistoryIndex = 0;
            }

            serializationRequested = false;

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serializing {eventsQueueIndex} event(s), eventTransmissionHistory is now {ArrayToString(eventTransmissionHistory)} with index {eventTransmissionHistoryIndex}");
        }

        public override void OnPostSerialization(SerializationResult result) => OnPostSerializationInternal(result.success, result.byteCount);

        // Version of OnPostSerialization which does not require instantiating SerializationResult, so that it can be called for debugging purposes.
        public void OnPostSerializationInternal(bool success, int byteCount)
        {
            // If there was a full sync in the queue, it should now have been transmitted at least once
            hasFullSyncReady = false;

            if (!Ready || !IsOwner || networkedData.Length == 0)
            {
                return;
            }

            if (!success)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialization failed! Tried to send {byteCount} bytes.");
                return;
            }

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialized with {networkedData.Length} bytes.\nBytes sent:\n{ArrayToString(networkedData)}");

            // Remove data from the buffer that no longer needs to be (re)transmitted
            DequeueEventsFromBuffer(eventTransmissionHistory[eventTransmissionHistoryIndex]);
            eventTransmissionHistory[eventTransmissionHistoryIndex] = 0; // Prevent trying to remove the same events again
            eventsQueueIndexAtLastTransmission = eventsQueueIndex;

            networkedData = new byte[0];

            lastEventTransmissionTime = targetTicks;
        }
        #endregion

        #region Receiver
        public void RequestEvent(NetworkEventType eventType)
        {
            if (!Ready)
            {
                return;
            }

            if (IsOwner)
            {
                Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(RequestEvent)} while we are the owner!");
                return;
            }

            SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "RequestEventReceived", eventType);
            lastEventTransmissionTime = SyncedTimeTicks;

            if (tester != null)
            {
                tester.RequestEvent(eventType);
            }
        }

        private void StoreIncomingData()
        {
            if (networkedData == null || networkedData.Length == 0)
            {
                return; // Nothing to store
            }

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Received {networkedData.Length} bytes!\nBytes received:\n{ArrayToString(networkedData)}");

            var length = networkedData.Length;
            int index = 0;
            int eventSize = 0; // Store event size here so we can increment the index no matter how we increment the loop
            while ((index += eventSize) < length)
            {
                if (length - index < HeaderLength) // Check package is at least as long as the minimum length possible. Reset package is also larger than HeaderLength.
                {
                    Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Remaining data in networkedData is not long enough to form a complete event! remaining: {length - index}.");
                    HandleError(false);
                    return;
                }

                eventSize = GetEventSizeFromHeader(networkedData, index);
                bool isReset = eventSize == 0;

                if (isReset)
                {
                    eventSize = ResetEventSize;
                }

                if (length - index < eventSize)
                {
                    Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Event size is larger than total remaining data! {nameof(eventSize)}: {eventSize}, remaining: {length - index}.");
                    HandleError(false);
                    return;
                }

                if (length - index < HeaderLength)
                {
                    Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Event size is not long enough to form a complete event! {nameof(eventSize)}: {eventSize}, minimum needed: {HeaderLength}.");
                    HandleError(false);
                    return;
                }

                var @event = GetArrayPart(networkedData, index, eventSize);

                if (isReset)
                {
                    TryApplyReset(@event);
                    return;
                }

                var timestamp = GetTimestampFromHeader(@event);
                var eventId = GetEventIdFromHeader(@event);
                var eventType = GetEventTypeFromHeader(@event);

                if (Synced || hasFullSyncReady)
                {
                    // Check if event id is sequential
                    if (eventId != GetNextEventId(lastEventId))
                    {
                        if (index + eventSize >= length // If this is the last event of the batch
                            && eventId != lastEventId) // Unless the eventId is the same, then this is probably just a duplicate
                        {
                            // The last event of the batch has to be synced up with our current event id count, else we've missed an event
                            Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) EventIds were not sequential! Did we miss an event? Timestamp: {timestamp}, eventId: {eventId}, lastEventId: {lastEventId}.");
                            HandleError(false);
                            return;
                        }

                        // We've likely already processed this event, ignore it
                        continue;
                    }

                    if (!QueueEventInBuffer(@event))
                    {
                        return;
                    }

                    if (eventsQueueIndex == 1) // If this is the next upcoming event, update the next event time
                    {
                        UpdateNextEventTime();
                    }

                    //Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)} Queued event with id {eventId}");
                }
                else if (eventType == NetworkEventType.FullSync) // If we're not yet synced, we only care about full sync events.
                {
                    QueueFullSyncForReplay(@event); // Immediately process full sync
                }
                else
                {
                    return;
                }

                lastEventId = eventId;
                lastEventTransmissionTime = SyncedTimeTicks;
            }
        }

        private void QueueFullSyncForReplay(byte[] @event)
        {
            // Clear buffer and put the full sync into it
            ClearBuffer();
            QueueEventInBuffer(@event);

            // Set this event to play after the default delay
            nextEventTime = targetTicks + delay;

            hasFullSyncReady = true;

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Queued full sync in buffer, should execute at {nextEventTime}.");
        }

        private void TryApplyReset(byte[] @event)
        {
            // Compare the guid to the one in the previous reset event
            if (IsDuplicateResetEvent(@event))
            {
                Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Rejected Reset event as it was a duplicate.");
                return;
            }

            Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Applied Reset event!");

            lastResetEventData = @event;
            ClearBuffer();
            SetSynced(false);
        }

        private bool IsDuplicateResetEvent(byte[] @event)
        {
            for (int i = HeaderTimestampIndex; i < ResetEventSize; i++)
            {
                if (@event[i] != lastResetEventData[i])
                {
                    return false;
                }
            }

            return true;
        }

        private void SetSynced(bool synced)
        {
            Synced = synced;

            if (!Synced)
            {
                Synced = false;
                RequestEvent(NetworkEventType.FullSync);
                lastEventTransmissionTime = SyncedTimeTicks;
            }
        }

        private void ApplyReceivedEvents()
        {
            IsEventUpdate = true;

            while (eventsQueueIndex > 0 && nextEventTime <= SyncedTimeTicks)
            {
                var success = ApplyEvent(eventsQueue[0]);

                if (!success)
                {
                    return;
                }

                DequeueEventsFromBuffer(1);
                UpdateNextEventTime();
            }
        }

        private bool ApplyEvent(byte[] @event)
        {
            var timestamp = GetTimestampFromHeader(@event);
            var eventType = GetEventTypeFromHeader(@event);

            if (!Synced)
            {
                SyncToTimestamp(timestamp);
                hasFullSyncReady = false;
                SetSynced(true);
            }
            else if (timestamp < SyncedTimeTicks)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Next received event is in the past! " +
                    $"{nameof(nextEventTime)}: {nextEventTime} < {nameof(SyncedTimeTicks)}: {SyncedTimeTicks}.");
                HandleError(true);
                return false;
            }

            var index = (int)HeaderLength; // Skip header

            var subscribers = GetEventSubscribers(eventType);

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) ApplyEvent with dt {SyncedDeltaTime}");

            if (subscribers != null)
            {
                foreach (var obj in subscribers)
                {
                    var success = obj.WriteSyncedData(@event, ref index, eventType);

                    if (!success)
                    {
                        Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Malformed data reported by {obj.name} during event type {eventType}!");
                        HandleError(true);
                        return false;
                    }
                }
            }

            var eventSize = GetEventSizeFromHeader(@event);
            if (index != eventSize)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Amount of data read does not match event size! Expected {eventSize}, read {index}.");
                HandleError(true);
                return false;
            }

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Performed incoming event of type {eventType}! Total {index} bytes.");

            retriesWithoutSuccess = 0; // We had success!

            return true;
        }

        public override void OnDeserialization()
        {
            if (!Ready || IsOwner)
            {
                return;
            }

            StoreIncomingData();
        }
        #endregion

        #region Buffer
        private void ClearBuffer()
        {
            eventsQueue = new byte[BufferMaxTotalEvents][];
            eventsQueueIndex = 0;
            lastEventId = 0;
            hasFullSyncReady = false;

            eventTransmissionHistory = new int[maxEventSendTries];
            eventTransmissionHistoryIndex = 0;
            eventsQueueIndexAtLastTransmission = 0;

            eventsToSendEarly = new NetworkEventType[BufferMaxTotalEvents];
            eventsToSendEarlyIndex = 0;
            eventsToSendLate = new NetworkEventType[BufferMaxTotalEvents];
            eventsToSendLateIndex = 0;
        }

        private void DequeueEventsFromBuffer(int eventCount)
        {
            if (eventCount > eventsQueueIndex) // Never remove more events than are in the queue
            {
                eventCount = eventsQueueIndex;
            }

            var oldBuffer = eventsQueue;
            eventsQueueIndex -= eventCount;
            eventsQueue = new byte[BufferMaxTotalEvents][];
            Array.Copy(oldBuffer, eventCount, eventsQueue, 0, eventsQueueIndex);
        }

        private bool QueueEventInBuffer(byte[] @event)
        {
            if (eventsQueueIndex >= BufferMaxTotalEvents)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer not large enough to store event! Maximum event count: {BufferMaxTotalEvents}.");
                HandleError(true);
                return false;
            }

            eventsQueue[eventsQueueIndex++] = @event;
            return true;
        }
        #endregion

        #region Time
        private void UpdateInternalTime()
        {
            if (paused && !stepNext)
            {
                return;
            }

            if (paused && stepNext)
            {
                targetTicks++;
                stepNext = false;
                return;
            }

            targetTicks = (int)((Time.fixedTime - startTime) / tickDelta);
        }

        private void ProgressSyncedTime()
        {
            SyncedTimeTicks++;
            SyncedTime = SyncedTimeTicks * tickDelta;
        }

        private void SyncToTimestamp(int timestamp)
        {
            startTime = Time.fixedTime - timestamp * tickDelta;
            targetTicks = timestamp;
            SyncedTimeTicks = timestamp;
            SyncedTime = SyncedTimeTicks * tickDelta;
            nextEventTime = timestamp;

            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Synced to timestamp {timestamp}, internalTime is now {targetTicks}, SyncedTime is now {SyncedTime}, nextEventTime is now {nextEventTime}");
        }

        private void UpdateNextEventTime()
        {
            if (eventsQueueIndex == 0)
            {
                return;
            }

            var nextEventTime = GetTimestampFromHeader(eventsQueue[0]);

            if (nextEventTime < this.nextEventTime)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event is earlier than previous event!");
                HandleError(true);
                return;
            }

            if (nextEventTime < SyncedTimeTicks)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event timestamp is earlier than our current synced time by {SyncedTime - nextEventTime} seconds! nextEventTime: {nextEventTime} SyncedTime: {SyncedTime}, internalTime: {targetTicks}");
                HandleError(true);
                return;
            }

            this.nextEventTime = nextEventTime;
        }
        #endregion

        #region Header
        private static byte GetNextEventId(byte currentEventId)
        {
            if (currentEventId == byte.MaxValue) // Udon forces overflow checks
            {
                return 0;
            }

            currentEventId += 1;

            return currentEventId;
        }

        private static ushort GetEventSizeFromHeader(byte[] @event, int eventIndex = 0)
            => BitConverter.ToUInt16(@event, eventIndex + HeaderEventSizeIndex);

        private static NetworkEventType GetEventTypeFromHeader(byte[] @event, int eventIndex = 0) =>
            (NetworkEventType)@event[eventIndex + HeaderEventTypeIndex];

        private static int GetTimestampFromHeader(byte[] @event, int eventIndex = 0) =>
            BitConverter.ToInt32(@event, eventIndex + HeaderTimestampIndex);

        private static byte GetEventIdFromHeader(byte[] @event, int eventIndex = 0) =>
            @event[eventIndex + HeaderEventIdIndex];
        #endregion

        #region VRC events
        public override void OnOwnershipTransferred(VRCPlayerApi newOwner)
        {
            if (!Ready)
            {
                return;
            }

            bool newOwnerIsLocalPlayer = newOwner == Networking.LocalPlayer;
            IsOwner = newOwnerIsLocalPlayer;

            if (newOwnerIsLocalPlayer)
            {
                ClearBuffer();
                SendReset();
            }
        }
        #endregion

        #region Utils
        public string ArrayToString(byte[] bytes)
        {
            var sb = new StringBuilder("[ ");
            foreach (var b in bytes)
            {
                sb.Append(b + " ");
            }
            sb.Append("]");
            return sb.ToString();
        }

        public string ArrayToString(int[] bytes)
        {
            var sb = new StringBuilder("[ ");
            foreach (var b in bytes)
            {
                sb.Append(b + " ");
            }
            sb.Append("]");
            return sb.ToString();
        }

        private static int GetFlattenedSize(byte[][] data, int start, int length)
        {
            var size = 0;
            for (int i = start; i < start + length; i++)
            {
                size += data[i].Length;
            }
            return size;
        }

        private static byte[] Flatten(byte[][] data, int start, int length)
        {
            var finalLength = GetFlattenedSize(data, start, length);

            var result = new byte[finalLength];
            int resultIndex = 0;

            for (int sourceIndex = start; sourceIndex < start + length; sourceIndex++)
            {
                var array = data[sourceIndex];
                Array.Copy(array, 0, result, resultIndex, array.Length);
                resultIndex += array.Length;
            }

            return result;
        }

        private static byte[] GetArrayPart(byte[] data, int start, int length)
        {
            var result = new byte[length];
            Array.Copy(data, start, result, 0, length);
            return result;
        }
        #endregion

        #region SyncedData
        public override void CollectSyncedData(byte[] data, ref int index, NetworkEventType eventType)
        {
            // Nothing
        }

        public override bool WriteSyncedData(byte[] data, ref int index, NetworkEventType eventType)
        {
            switch (eventType)
            {
                case NetworkEventType.Pause:
                    Pause();
                    break;
                case NetworkEventType.Resume:
                    Resume();
                    break;
                case NetworkEventType.Step:
                    Step();
                    break;
            }

            return true;
        }
        #endregion

        #region Debug
        public void SimulateSyncToTimestamp(float timestamp)
        {
            //SyncToTimestamp(timestamp);
        }

        public void WriteDebugOutput(TMP_InputField debugOutput)
        {
            debugOutput.text = $"{nameof(NetworkManager)}:\n" +
                $"IsOwner: {IsOwner}\n" +
                $"Ready: {Ready}\n" +
                $"Synced: {Synced}\n" +
                $"hasFullSyncReady: {hasFullSyncReady}\n" +
                $"lastEventId: {lastEventId}\n" +
                $"Time.fixedTime: {Time.fixedTime}\n" +
                $"startTime: {startTime}\n" +
                $"targetTicks: {targetTicks}\n" +
                $"SyncedTime: {SyncedTime}\n" +
                $"Dt: {SyncedDeltaTime}\n" +
                $"BufferIndex: {eventsQueueIndex}\n" +
                $"BufferIndexHistory: {ArrayToString(eventTransmissionHistory)}\n" +
                $"\n";
        }

        /// <summary>
        /// Text field to display debug info in.
        /// </summary>
        [SerializeField] private TMP_InputField debugOutput;

        /// <summary>
        /// An animator which visualizes whether the current perspective is the owner.
        /// </summary>
        [SerializeField] private Animator DebugImageToIndicateOwner;

        /// <summary>
        /// An animator which visualizes whether the NetworkManager is synced.
        /// </summary>
        [SerializeField] private Animator DebugImageToIndicateSynced;

        /// <summary>
        /// An animator which visualizes whether the NetworkManager is synced.
        /// </summary>
        [SerializeField] private Animator DebugImageToIndicateReady;

        private NetworkManagerTester tester;

        public void SetNetworkManagerTester(NetworkManagerTester tester)
        {
            this.tester = tester;
        }

        public byte[] NetworkedData
        {
            get => networkedData;
            set => networkedData = value;
        }

        public bool SerializationRequested => serializationRequested;

        public void SetIsOwner(bool isOwner) => IsOwner = isOwner;

        public void DoFullSync()
        {
            SendEventSoon(NetworkEventType.FullSync);
        }

        public void Pause()
        {
            paused = true;
            stepNext = false;
        }

        private void Resume()
        {
            paused = false;
            stepNext = false;
        }

        private void Step()
        {
            paused = true;
            stepNext = true;
        }

        public void PauseButtonPressed()
        {
            Pause();
            SendEventSoon(NetworkEventType.Pause);
        }

        public void ResumeButtonPressed()
        {
            Resume();
            SendEventSoon(NetworkEventType.Resume);
        }

        public void StepButtonPressed()
        {
            Step();
            SendEventSoon(NetworkEventType.Step);
        }
        #endregion
    }
}
