using System;
using System.Collections.Generic;
using System.Globalization;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Network;
using TriInspector;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.ResourceManagement.AsyncOperations;
using Logger = RebootKit.Engine.Foundation.Logger;

namespace RebootKit.Engine.Simulation {
    public interface IActorData : ISerializableEntity { }

    public class NoActorData : IActorData {
        public void Serialize(NetworkBufferWriter writer) { }

        public void Deserialize(NetworkBufferReader reader) { }

        public int GetMaxBytes() {
            return 0;
        }
    }

    public struct ActorCommand : ISerializableEntity {
        public ushort ActorID;
        public byte CommandID;
        public NativeArray<byte> Data;

        public int GetMaxBytes() {
            return sizeof(ushort) + // ActorID
                   sizeof(byte) + // CommandID
                   sizeof(byte) + // Data length
                   sizeof(byte) * Data.Length; // Data
        }

        public void Serialize(NetworkBufferWriter writer) {
            writer.Write(ActorID);
            writer.Write(CommandID);
            writer.Write((byte) Data.Length);
            if (Data.IsCreated) {
                writer.Write(Data);
            }
        }

        public void Deserialize(NetworkBufferReader reader) {
            reader.Read(out ActorID);
            reader.Read(out CommandID);
            reader.Read(out byte dataLength);
            if (dataLength > 0) {
                reader.Read(out Data, dataLength, Allocator.Temp);
            }
        }
    }

    public struct ActorEvent : ISerializableEntity {
        public ushort ActorID;
        public byte EventID;
        public NativeArray<byte> Data;

        public int GetMaxBytes() {
            return sizeof(ushort) + // ActorID
                   sizeof(byte) + // EventID
                   sizeof(byte) + // Data length
                   sizeof(byte) * Data.Length; // Data
        }

        public void Serialize(NetworkBufferWriter writer) {
            writer.Write(ActorID);
            writer.Write(EventID);
            Assert.IsTrue(Data.Length < byte.MaxValue, "Data of ActorEvent is too large to fit in a byte.");

            writer.Write((byte) Data.Length);
            if (Data.IsCreated) {
                writer.Write(Data);
            }
        }

        public void Deserialize(NetworkBufferReader reader) {
            reader.Read(out ActorID);
            reader.Read(out EventID);
            reader.Read(out byte dataLength);

            if (dataLength > 0) {
                reader.Read(out Data, dataLength, Allocator.Temp);
            }
        }
    }

    [Flags]
    enum ActorFlags : byte {
        None = 0,
        Hidden = 1 << 0,
        DisableColliders = 1 << 1,
    }

    struct ActorCoreStateSnapshot : ISerializableEntity {
        public ushort ActorID;
        public DateTime Timestamp;

        // @NOTE: Position, Rotation, and Scale are in local space.
        public Vector3 Position;
        public Quaternion Rotation;
        public Vector3 Scale;

        public ActorFlags Flags;

        public ushort MasterActorID;
        public FixedString32Bytes MasterSocketName;

        public void Serialize(NetworkBufferWriter writer) {
            writer.Write(ActorID);
            writer.Write(Timestamp.Ticks);
            writer.Write(Position);
            writer.Write(Rotation);
            writer.Write(Scale);
            writer.Write((byte) Flags);
            writer.Write(MasterActorID);
            writer.Write(MasterSocketName);
        }

        public void Deserialize(NetworkBufferReader reader) {
            reader.Read(out ActorID);
            reader.Read(out long ticks);
            Timestamp = new DateTime(ticks, DateTimeKind.Utc);
            reader.Read(out Position);
            reader.Read(out Rotation);
            reader.Read(out Scale);
            reader.Read(out byte flagsByte);
            Flags = (ActorFlags) flagsByte;
            reader.Read(out MasterActorID);
            reader.Read(out MasterSocketName);
        }

        public int GetMaxBytes() {
            return sizeof(ushort) + // ActorID
                   sizeof(long) + // Timestamp
                   sizeof(float) * 3 + // Position
                   sizeof(float) * 4 + // Rotation (Quaternion)
                   sizeof(float) * 3 + // Scale
                   sizeof(byte) + // Flags
                   sizeof(ushort) + // MasterActorID
                   sizeof(byte) * 32; // MasterSocketName
        }
    }

    [Flags]
    public enum ActorTransformSyncMode : byte {
        None = 0,
        Position = 1 << 0,
        Rotation = 1 << 1,
        Scale = 1 << 2
    }

    public struct ActorTransformSyncData : ISerializableEntity {
        public ushort ActorID;
        public ActorTransformSyncMode SyncMode;

        public Vector3 Position;
        public Vector3 Rotation;
        public Vector3 Scale;

        public void Serialize(NetworkBufferWriter writer) {
            writer.Write(ActorID);
            writer.Write((byte) SyncMode);

            if ((SyncMode & ActorTransformSyncMode.Position) != 0) {
                writer.Write(Position);
            }

            if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) {
                Rotation.x = Mathf.Repeat(Rotation.x, 360.0f);
                Rotation.y = Mathf.Repeat(Rotation.y, 360.0f);
                Rotation.z = Mathf.Repeat(Rotation.z, 360.0f);

                ushort rotX = QuantizationUtility.FloatToUShort(Rotation.x, 0.0f, 360.0f);
                ushort rotY = QuantizationUtility.FloatToUShort(Rotation.y, 0.0f, 360.0f);
                ushort rotZ = QuantizationUtility.FloatToUShort(Rotation.z, 0.0f, 360.0f);

                writer.Write(rotX);
                writer.Write(rotY);
                writer.Write(rotZ);
            }

            if ((SyncMode & ActorTransformSyncMode.Scale) != 0) {
                writer.Write(Scale);
            }
        }

        public void Deserialize(NetworkBufferReader reader) {
            reader.Read(out ActorID);
            reader.Read(out byte syncModeByte);
            SyncMode = (ActorTransformSyncMode) syncModeByte;

            if ((SyncMode & ActorTransformSyncMode.Position) != 0) {
                reader.Read(out Position);
            }

            if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) {
                reader.Read(out ushort rotX);
                reader.Read(out ushort rotY);
                reader.Read(out ushort rotZ);

                Rotation.x = QuantizationUtility.UShortToFloat(rotX, 0.0f, 360.0f);
                Rotation.y = QuantizationUtility.UShortToFloat(rotY, 0.0f, 360.0f);
                Rotation.z = QuantizationUtility.UShortToFloat(rotZ, 0.0f, 360.0f);
            }

            if ((SyncMode & ActorTransformSyncMode.Scale) != 0) {
                reader.Read(out Vector3 scale);
            }
        }

        public int GetMaxBytes() {
            int size = sizeof(byte); // SyncMode

            if ((SyncMode & ActorTransformSyncMode.Position) != 0) {
                size += sizeof(float) * 3; // Vector3
            }

            if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) {
                size += sizeof(ushort) * 3; // Vector3 - Euler angles
            }

            if ((SyncMode & ActorTransformSyncMode.Scale) != 0) {
                size += sizeof(float) * 3; // Vector3
            }

            return size;
        }
    }

    [DeclareFoldoutGroup("Actor")]
    public abstract class Actor : MonoBehaviour {
        static readonly Logger s_ActorLogger = new Logger(nameof(Actor));

        [field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public string SourceActorPath { get; internal set; } = "";
        [field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public ulong ActorStaticID { get; internal set; }
        [field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public ushort ActorID { get; internal set; }

        [NonSerialized] internal IActorData Data;

        [SerializeField, Group("Actor")] string m_ActorName = "";

        public string ActorName {
            get {
                return m_ActorName;
            }
        }

        internal bool IsLocalOnly;

        [SerializeField, Group("Actor")] internal Rigidbody actorRigidbody;
        [InfoBox("If empty, will use GetComponentsInChildren<Collider>() to find colliders.")]
        [SerializeField, Group("Actor")] Collider[] m_OverrideActorColliders;

        [SerializeField, Group("Actor")] bool m_SetKinematicOnMount = true;
        [SerializeField, Group("Actor")] bool m_DisableCollidersOnMount = true;

        internal ActorFlags Flags = ActorFlags.None;

        // @NOTE: Sync won't happen if actor is mounted to another actor.
        [SerializeField, Group("Actor")] internal bool syncTransform = true;
        [SerializeField, Group("Actor")] internal bool syncPosition = true;
        [SerializeField, Group("Actor")] internal bool syncRotation = true;
        [SerializeField, Group("Actor")] internal bool syncScale = false;

        class ActorClientState {
            public ulong LastSyncTick;
            public Vector3 Position;
            public Quaternion Rotation;
            public Vector3 Scale;
        }
 
        readonly Dictionary<ulong, ActorClientState> m_ClientsStates = new Dictionary<ulong, ActorClientState>();

        [Serializable]
        public struct AttachmentSocket {
            [InfoBox("Max length: 32")]
            public string socketName;
            public Transform root;

            public Vector3 localPosition;
            public Quaternion localRotation;
        }

        [SerializeField, Group("Actor")] AttachmentSocket[] m_AttachmentSockets;

        // @NOTE: Master actor is the actor that this actor is attached to, if any.
        internal Actor MasterActor;
        internal FixedString32Bytes MasterSocketName;

        internal bool IsCoreStateDirty;
        public bool IsDataDirty { get; protected internal set; }

        internal ActorsManager Manager;
        internal AsyncOperationHandle<GameObject> AssetHandle;

        internal DateTime LastCoreStateSyncTime = DateTime.MinValue;

        //
        // @MARK: Unity callbacks
        //
        void OnValidate() {
            if (ActorStaticID == 0) {
                ActorStaticID = UniqueID.NewULongFromGuid();
            }
        }

        //
        // @MARK: Callbacks to override in derived classes
        //

        protected abstract IActorData CreateActorData();

        // @MARK: Server side
        public virtual void OnServerTick(float deltaTime) { }
        protected virtual void OnActorCommandServer(ulong senderID, ActorCommand actorCommand) { }

        // Override this method to implement client-side logic
        public virtual void OnClientTick(float deltaTime) { }
        protected virtual void OnActorEventClient(ActorEvent actorEvent) { }

        //
        // @MARK: Server API
        //
        public void SetHidden(bool hidden) {
            if (!RR.IsServer()) {
                s_ActorLogger.Error($"Only the server can set actor visibility. Actor: {name} (ID: {ActorID})");
                return;
            }

            bool shouldBeActive = !hidden;
            if (gameObject.activeSelf == shouldBeActive) {
                s_ActorLogger.Warning($"Actor {name} (ID: {ActorID}) is already in the desired visibility state: {shouldBeActive.ToString()}");
                return;
            }

            gameObject.SetActive(shouldBeActive);

            if (hidden) {
                Flags |= ActorFlags.Hidden;
            } else {
                Flags &= ~ActorFlags.Hidden;
            }

            IsCoreStateDirty = true;
        }

        public void MountTo(Actor actor, string slotName) {
            if (!RR.IsServer() && !IsLocalOnly) {
                s_ActorLogger.Error($"Only the server can mount actors. Actor: {name} (ID: {ActorID})");
                return;
            }

            if (actor.TryGetAttachmentSocket(slotName, out AttachmentSocket _)) {
                MasterActor = actor;
                MasterSocketName = new FixedString32Bytes(slotName);

                if (m_DisableCollidersOnMount) {
                    Flags |= ActorFlags.DisableColliders;
                    UpdateLocalCollidersState(false);
                }

                if (m_SetKinematicOnMount) {
                    actorRigidbody.isKinematic = true;
                }

                UpdateMountedTransform();
                IsCoreStateDirty = true;
            }
        }

        public void UnMount() {
            if (!RR.IsServer() && !IsLocalOnly) {
                s_ActorLogger.Error($"Only the server can unmount actors. Actor: {name} (ID: {ActorID})");
                return;
            }

            if (MasterActor == null) {
                s_ActorLogger.Error($"Actor {name} (ID: {ActorID}) is not mounted to any actor.");
                return;
            }

            MasterActor = null;
            MasterSocketName = default;
            UpdateMountedTransform();

            if (m_DisableCollidersOnMount) {
                UpdateLocalCollidersState(true);
                Flags &= ~ActorFlags.DisableColliders;
            }
            
            if (m_SetKinematicOnMount) {
                actorRigidbody.isKinematic = false;
            }

            IsCoreStateDirty = true;
        }

        //
        // @MARK: Common API
        //
        public bool IsHidden() {
            return !gameObject.activeSelf;
        }

        protected void SendActorCommand<TCmdData>(byte commandID, ref TCmdData commandData)
            where TCmdData : struct, ISerializableEntity {
            NativeArray<byte> data = DataSerializationUtils.Serialize(commandData);

            SendActorCommand(commandID, data);
        }

        protected void SendActorCommand(byte commandID, NativeArray<byte> data = default) {
            if (Manager is null) {
                s_ActorLogger.Error($"Cannot send command because Manager is null for actor {name} (ID: {ActorID})");
                return;
            }

            var command = new ActorCommand {
                ActorID = ActorID,
                CommandID = commandID,
                Data = data
            };

            Manager.SendActorCommand(command);
        }

        protected void SendActorEvent<TEventData>(byte eventID, ref TEventData eventData)
            where TEventData : struct, ISerializableEntity {
            NativeArray<byte> data = DataSerializationUtils.Serialize(eventData);
            SendActorEvent(eventID, data);
        }

        protected void SendActorEvent(byte eventID, NativeArray<byte> data = default) {
            if (!RR.IsServer()) {
                s_ActorLogger.Error($"Only the server can send actor events. Actor: {name} (ID: {ActorID})");
                return;
            }

            if (Manager == null) {
                s_ActorLogger.Error($"Cannot send event because Manager is null for actor {name} (ID: {ActorID})");
                return;
            }

            var actorEvent = new ActorEvent {
                ActorID = ActorID,
                EventID = eventID,
                Data = data
            };

            Manager.SendActorEvent(actorEvent);
        }

        protected T DataAs<T>() where T : IActorData {
            if (Data is T data) {
                return data;
            }

            throw new InvalidCastException($"Actor data is not of type {typeof(T).Name}");
        }

        bool TryGetAttachmentSocket(string socketName, out AttachmentSocket socket) {
            s_ActorLogger.Info("Getting attachment socket: " + socketName);
            foreach (AttachmentSocket attachmentSocket in m_AttachmentSockets) {
                if (attachmentSocket.socketName.Equals(socketName, StringComparison.Ordinal)) {
                    socket = attachmentSocket;
                    return true;
                }
            }

            socket = default;
            return false;
        }

        //
        // @MARK: Internal
        //
        internal void InitializeOnClient() {
            if (actorRigidbody != null) {
                actorRigidbody.isKinematic = true;
            }
        }
        
        internal ActorCoreStateSnapshot GetCoreStateSnapshot() {
            var snapshot = new ActorCoreStateSnapshot {
                ActorID = ActorID,
                Timestamp = DateTime.UtcNow,
                Position = transform.localPosition,
                Rotation = transform.localRotation,
                Scale = transform.localScale,
                Flags = Flags,
                MasterActorID = MasterActor != null ? MasterActor.ActorID : (ushort)0
            };

            if (snapshot.MasterActorID != 0) {
                snapshot.MasterSocketName = MasterSocketName;
            } else {
                snapshot.MasterSocketName = default;
            }

            return snapshot;
        }

        internal void RestoreCoreState(ActorCoreStateSnapshot snapshot) {
            if (snapshot.Timestamp < LastCoreStateSyncTime) {
                s_ActorLogger.Warning($"Received an outdated core state snapshot for actor {name} (ID: {ActorID}). " +
                                      $"Current time: {DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)}, Snapshot time: {snapshot.Timestamp}");
                return;
            }
            LastCoreStateSyncTime = snapshot.Timestamp;
            Flags = snapshot.Flags;

            if (snapshot.MasterActorID != 0) {
                MasterActor = RR.FindSpawnedActor(snapshot.MasterActorID);
                MasterSocketName = snapshot.MasterSocketName;
                UpdateMountedTransform();
            } else {
                MasterActor = null;
                MasterSocketName = default;

                UpdateMountedTransform();
                transform.localPosition = snapshot.Position;
                transform.localRotation = snapshot.Rotation;
                transform.localScale = snapshot.Scale;
            }

            if ((snapshot.Flags & ActorFlags.Hidden) != 0) {
                gameObject.SetActive(false);
            } else {
                gameObject.SetActive(true);
            }
            
            bool enableColliders = (Flags & ActorFlags.DisableColliders) == 0;
            UpdateLocalCollidersState(enableColliders);
        }

        ActorClientState GetActorClientState(ulong clientID) {
            if (m_ClientsStates.TryGetValue(clientID, out ActorClientState clientState)) {
                return clientState;
            }

            clientState = new ActorClientState {
                LastSyncTick = 0,
                Position = transform.localPosition,
                Rotation = transform.localRotation,
                Scale = transform.localScale
            };
            m_ClientsStates[clientID] = clientState;

            return clientState;
        }
        
        internal ulong GetLastSyncTick(ulong clientID) {
            ActorClientState actorClientState = GetActorClientState(clientID);
            return actorClientState.LastSyncTick;
        }

        internal void UpdateClientState(ulong clientID, ulong serverTick) {
            ActorClientState actorClientState = GetActorClientState(clientID);
            actorClientState.LastSyncTick = serverTick;
            actorClientState.Position = transform.localPosition;
            actorClientState.Rotation = transform.localRotation;
            actorClientState.Scale = transform.localScale;
        }
        
        internal ActorTransformSyncData GetTransformSyncDataForClient(ulong clientID) {
            var data = new ActorTransformSyncData {
                ActorID = ActorID,
                SyncMode = ActorTransformSyncMode.None
            };
            
            if (!syncTransform || MasterActor != null) {
                return data;
            }
            
            data.Position = transform.localPosition;
            data.Rotation = transform.localRotation.eulerAngles;
            data.Scale = transform.localScale;

            ActorClientState actorClientState = GetActorClientState(clientID);

            if (syncPosition && math.distancesq(actorClientState.Position, transform.localPosition) > 0.01f) {
                data.SyncMode |= ActorTransformSyncMode.Position;
            }
            
            if (syncRotation && Quaternion.Angle(actorClientState.Rotation, transform.localRotation) > 0.01f) {
                data.SyncMode |= ActorTransformSyncMode.Rotation;
            }

            if (syncScale && math.distancesq(actorClientState.Scale, transform.localScale) > 0.01f) {
                data.SyncMode |= ActorTransformSyncMode.Scale;
            }

            return data;
        }

        internal void RestoreTransformState(ActorTransformSyncData data) {
            bool useRigidbody = actorRigidbody != null;

            if ((data.SyncMode & ActorTransformSyncMode.Position) != 0) {
                if (useRigidbody) {
                    actorRigidbody.position = data.Position;
                } else {
                    transform.localPosition = data.Position;
                }
            }

            if ((data.SyncMode & ActorTransformSyncMode.Rotation) != 0) {
                if (useRigidbody) {
                    actorRigidbody.rotation = Quaternion.Euler(data.Rotation);
                } else {
                    transform.localRotation = Quaternion.Euler(data.Rotation);
                }
            }

            if ((data.SyncMode & ActorTransformSyncMode.Scale) != 0) {
                transform.localScale = data.Scale;
            }
        }

        void UpdateLocalCollidersState(bool enable) {
            Collider[] colliders = m_OverrideActorColliders.Length > 0 ? m_OverrideActorColliders
                                       : GetComponentsInChildren<Collider>();

            foreach (Collider col in colliders) {
                col.enabled = enable;
            }
        }

        void UpdateMountedTransform() {
            if (MasterActor == null) {
                transform.SetParent(null);
                return;
            }

            if (MasterActor.TryGetAttachmentSocket(MasterSocketName.Value, out AttachmentSocket socket)) {
                transform.SetParent(socket.root);
                transform.localPosition = socket.localPosition;
                transform.localRotation = socket.localRotation;
            } else {
                s_ActorLogger.Error($"Failed to update mounted transform: Socket {MasterSocketName} not found on {MasterActor.name}");
            }
        }

        internal IActorData InternalCreateActorData() {
            return CreateActorData();
        }

        internal void HandleActorCommand(ulong senderID, ActorCommand actorCommand) {
            if (!RR.IsServer()) {
                s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})");
                return;
            }

            if (Manager is null) {
                s_ActorLogger.Error($"Cannot handle command because Manager is null for actor {name} (ID: {ActorID})");
                return;
            }

            if (actorCommand.ActorID != ActorID) {
                s_ActorLogger.Error($"Actor command ActorID {actorCommand.ActorID} does not match this actor's ID {ActorID}");
                return;
            }

            OnActorCommandServer(senderID, actorCommand);
        }

        internal void HandleActorEvent(ActorEvent actorEvent) {
            if (Manager is null) {
                s_ActorLogger.Error($"Cannot handle event because Manager is null for actor {name} (ID: {ActorID})");
                return;
            }

            if (actorEvent.ActorID != ActorID) {
                s_ActorLogger.Error($"Actor event ActorID {actorEvent.ActorID} does not match this actor's ID {ActorID}");
                return;
            }

            OnActorEventClient(actorEvent);
        }
    }
}