using System;
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
    {
        FullSyncForced = 0,
        FullSync = 1,
        PacManTurn = 2,
        StartGameButtonPressed = 3,
    }

    public class NetworkManager : UdonSharpBehaviour
    {
        // 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 at which the receiving side replays events.
        /// </summary>
        [SerializeField] private float delay = 1f;
        /// <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 ping.
        /// </summary>
        [SerializeField] private float pingDelay = 0.3f;
        #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;
        #endregion

        #region Private attributes
        /// <summary>
        /// Objects which are controlled by this <see cref="NetworkManager"/>.
        /// </summary>
        private SyncedObject[] syncedObjects;

        /// <summary>
        /// Offset from system time to network time, including delay.
        /// </summary>
        private float offsetTime;
        /// <summary>
        /// Time since last full sync, captured when this FixedUpdate started, with network delay applied.
        /// </summary>
        private float internalTime;

        /// <summary>
        /// Time at which next received event occured.
        /// </summary>
        private float nextEventTime;

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

        /// <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[] eventsToSend;
        /// <summary>
        /// Index for <see cref="eventsToSend"/>
        /// </summary>
        private int eventsToSendIndex;

        /// <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.
        /// </summary>
        private float 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
        /// <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; private set; } = false;

        /// <summary>
        /// Whether the current perspective is synced with the owner. (Always true if current perspective is owner.)
        /// </summary>
        public bool Synced { get; private set; } = false;

        /// <summary>
        /// The time since last full sync which is currently being simulated.
        /// </summary>
        public float SyncedTime { get; private set; }

        /// <summary>
        /// Time since the last simulation, in seconds.
        /// </summary>
        public float SyncedDeltaTime { get; private set; }

        /// <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; private set; }
        #endregion

        #region General
        public void Awake()
        {
            // I would set the instance variable here, but it's somehow null by the time Initialize() is called... Udon moment?
            var syncedObjects = root.GetComponentsInChildren<SyncedObject>(includeInactive: true);
            foreach (var obj in syncedObjects)
            {
                obj.networkManager = this;
            }
        }

        public void Initialize()
        {
            if (!BitConverter.IsLittleEndian)
            {
                Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: NetworkManager only supports little endian! Network sync will not be possible.");
                Ready = false;
                return;
            }

            syncedObjects = root.GetComponentsInChildren<SyncedObject>(includeInactive: true);

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

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

            SetOwner(Networking.IsOwner(gameObject));

            ClearBuffer();

            Synced = IsOwner; // Owner is always synced
            retriesWithoutSuccess = 0;
            hasFullSyncReady = false;

            offsetTime = Time.fixedTime;
            internalTime = 0;
            SyncedTime = 0;
            SyncedDeltaTime = Time.fixedDeltaTime;

            Ready = true;

            Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Initialized, time offset: {offsetTime}");
        }

        public void FixedUpdate()
        {
            if (!Ready)
            {
                return;
            }

            // Fetch the current time
            UpdateInternalTime();

            if (Ready)
            {
                if (IsOwner)
                {
                    ProgressPingTime(); // See if we need to send a ping
                }
                else
                {
                    ProgressEventTime(); // See if there's events that need to be replayed
                }
            }

            // Forwards simulated time at the FixedUpdate pace
            PerformFixedSyncedUpdate();

            if (IsOwner)
            {
                ProcessEventsToSend();
            }
        }

        private void UpdateInternalTime()
        {
            internalTime = Time.fixedTime - offsetTime;
        }

        private void PerformFixedSyncedUpdate()
        {
            IsEventUpdate = false;
            ProgressSyncedTime(internalTime);

            for (int i = 0; i < syncedObjects.Length; i++)
            {
                var obj = syncedObjects[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();
            }

            Synced = false;

            if (!IsOwner)
            {
                RequestEvent(NetworkEventType.FullSync);
            }
            else
            {
                SendEventSoon(NetworkEventType.FullSyncForced);
            }
        }

        private void SetOwner(bool isOwner)
        {
            IsOwner = isOwner;

            if (DebugImageToIndicateOwner != null)
            {
                DebugImageToIndicateOwner.SetFloat("Color", isOwner ? 1 : 0);
            }
        }
        #endregion

        #region Sender
        public void SendEventSoon(NetworkEventType eventType)
        {
            if (!Ready)
            {
                return;
            }

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

            if (eventsQueueIndex > eventsToSend.Length)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(eventsToSend)} overflow!");
                HandleError(false);
                return;
            }

            eventsToSend[eventsToSendIndex++] = eventType;
        }

        private void ProcessEventsToSend()
        {
            for (int i = 0; i < eventsToSendIndex; i++)
            {
                SendEventNow(eventsToSend[0]);
            }

            eventsToSendIndex = 0;
        }

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

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

            var timestamp = SyncedTime;

            var eventId = GetNextEventId(lastEventId);

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

            foreach (var obj in syncedObjects)
            {
                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);

            if (IsFullSync(eventType))
            {
                hasFullSyncReady = true;
            }

            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, float 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] = Int32ToByte((int)eventType);
            index = HeaderLength;
        }

        private void RequestSerializationForEvents()
        {
            RequestSerialization();
            serializationRequested = true;
        }

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

            if (IsFullSync(eventType) && hasFullSyncReady)
            {
                return; // Don't send another full sync if we're already preparing to send one
            }

            if (eventType == NetworkEventType.FullSyncForced)
            {
                SendEventSoon(NetworkEventType.FullSync); // Remote is not allowed to request a forced full sync
                return;
            }

            SendEventSoon(eventType);
        }

        private void ProgressPingTime()
        {
            if (eventsQueueIndex > 0 
                && internalTime - lastEventTransmissionTime >= pingDelay)
            {
                RequestSerializationForEvents();
            }
        }

        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)
        {
            if (!Ready || !IsOwner || networkedData.Length == 0)
            {
                return;
            }

            if (!result.success)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialization failed! Tried to send {result.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];

            // If there was a full sync in the queue, it has now been transmitted at least once
            hasFullSyncReady = false;

            lastEventTransmissionTime = internalTime;
        }
        #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);
        }

        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)
                {
                    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);

                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);

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

                if (eventType != NetworkEventType.FullSyncForced && (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;
                    }

                    QueueEventInBuffer(@event);

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

                lastEventId = eventId;
            }

            if (Synced)
            {
                Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)} blaat");
                UpdateNextEventTime();
            }
        }

        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 = internalTime + delay;

            hasFullSyncReady = true;

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

        private void ProgressEventTime()
        {
            IsEventUpdate = true;

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

                if (!success)
                {
                    return;
                }

                DequeueEventsFromBuffer(1);
                Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)} damn");
                UpdateNextEventTime();
            }
        }

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

            var isFullSync = IsFullSync(eventType);

            if (!Synced || eventType == NetworkEventType.FullSyncForced)
            {
                SyncToTimestamp(timestamp);
            }

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

            ProgressSyncedTime(timestamp);

            foreach (var obj in syncedObjects)
            {
                obj.SyncedUpdate();
            }

            foreach (var obj in syncedObjects)
            {
                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;
            }

            if (!Synced && isFullSync)
            {
                hasFullSyncReady = false;
                Synced = true;
            }

            Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Processed incoming event! 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;
            eventsToSend = new NetworkEventType[BufferMaxTotalEvents];
            eventsToSendIndex = 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 ProgressSyncedTime(float newTime)
        {
            //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) updating SyncedTime from {SyncedTime} to {newTime}");
            SyncedDeltaTime = newTime - SyncedTime;

            if (SyncedDeltaTime < 0)
            {
                Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Negative Dt: {SyncedDeltaTime}! Going from {SyncedTime} to {newTime}, IsEventUpdate: {IsEventUpdate}");
            }

            SyncedTime = newTime;
        }

        private void SyncToTimestamp(float timestamp)
        {
            var oldOffset = offsetTime;
            offsetTime = Time.fixedTime - timestamp;

            var delta = offsetTime - oldOffset;
            internalTime -= delta;
            SyncedTime -= delta;
            nextEventTime -= delta;

            Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Synced to timestamp {timestamp}, current time is {Time.fixedTime}, offsetTime is now {offsetTime}, internalTime is now {internalTime}, 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 < SyncedTime)
            {
                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: {internalTime}");
                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 bool IsFullSync(NetworkEventType eventType)
            => eventType == NetworkEventType.FullSync || eventType == NetworkEventType.FullSyncForced;

        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 float GetTimestampFromHeader(byte[] @event, int eventIndex = 0) =>
            BitConverter.ToSingle(@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;
            SetOwner(newOwnerIsLocalPlayer);

            if (newOwnerIsLocalPlayer)
            {
                SendEventSoon(NetworkEventType.FullSyncForced);
            }
        }
        #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;
        }

        public static byte Int32ToByte(int value) =>
            (byte)value; // Doing this inline causes an error...?
        #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}" +
                $"Time.fixedTime: {Time.fixedTime}\n" +
                $"offsetTime: {offsetTime}\n" +
                $"internalTime: {internalTime}\n" +
                $"SyncedTime: {SyncedTime}\n" +
                $"Dt: {SyncedDeltaTime}\n" +
                $"BufferIndex: {eventsQueueIndex}\n" +
                $"BufferIndexHistory: {ArrayToString(eventTransmissionHistory)}\n" +
                $"\n";
        }

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

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