using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using UnityEngine.InputSystem.Utilities;
using Unity.Collections.LowLevel.Unsafe;

namespace UnityEngine.InputSystem.LowLevel
{
    /// <summary>
    /// A complete state snapshot for an entire input device.
    /// </summary>
    /// <remarks>
    /// This is a variable-sized event.
    /// </remarks>
    [StructLayout(LayoutKind.Explicit, Size = InputEvent.kBaseEventSize + 4 + kStateDataSizeToSubtract, Pack = 1)]
    public unsafe struct StateEvent : IInputEventTypeInfo
    {
        public const int Type = 0x53544154; // 'STAT'

        internal const int kStateDataSizeToSubtract = 1;

        [FieldOffset(0)]
        public InputEvent baseEvent;

        /// <summary>
        /// Type code for the state stored in the event.
        /// </summary>
        [FieldOffset(InputEvent.kBaseEventSize)]
        public FourCC stateFormat;

        [FieldOffset(InputEvent.kBaseEventSize + sizeof(int))]
        internal fixed byte stateData[kStateDataSizeToSubtract]; // Variable-sized.

        public uint stateSizeInBytes => baseEvent.sizeInBytes - (InputEvent.kBaseEventSize + sizeof(int));

        public void* state
        {
            get
            {
                fixed(byte* data = stateData)
                {
                    return data;
                }
            }
        }

        public InputEventPtr ToEventPtr()
        {
            fixed(StateEvent * ptr = &this)
            {
                return new InputEventPtr((InputEvent*)ptr);
            }
        }

        public FourCC typeStatic => Type;

        /// <summary>
        /// Retrieve the state stored in the event.
        /// </summary>
        /// <typeparam name="TState">Type of state expected to be stored in the event. <see cref="IInputStateTypeInfo.format"/>
        /// must match <see cref="stateFormat"/>.</typeparam>
        /// <returns>Copy of the state stored in the event.</returns>
        /// <exception cref="InvalidOperationException"><see cref="stateFormat"/> does not match <see cref="IInputStateTypeInfo.format"/>
        /// of <typeparamref name="TState"/>.</exception>
        /// <remarks>
        /// The event may contain less or more data than what is found in the struct. Only the data found in the event
        /// is copied. The remainder of the struct is left at default values.
        /// </remarks>
        /// <seealso cref="GetState{T}(InputEventPtr)"/>
        public TState GetState<TState>()
            where TState : struct, IInputStateTypeInfo
        {
            var result = default(TState);
            if (stateFormat != result.format)
                throw new InvalidOperationException($"Expected state format '{result.format}' but got '{stateFormat}' instead");

            UnsafeUtility.MemCpy(UnsafeUtility.AddressOf(ref result), state, Math.Min(stateSizeInBytes, UnsafeUtility.SizeOf<TState>()));

            return result;
        }

        /// <summary>
        /// Retrieve the state stored in the event.
        /// </summary>
        /// <typeparam name="TState">Type of state expected to be stored in the event. <see cref="IInputStateTypeInfo.format"/>
        /// must match <see cref="stateFormat"/>.</typeparam>
        /// <param name="ptr">A pointer to an input event. The pointer is checked for <c>null</c> and
        /// for whether the type of event it refers to is indeed a StateEvent.</param>
        /// <returns>Copy of the state stored in the event.</returns>
        /// <remarks>
        /// The event may contain less or more data than what is found in the struct. Only the data found in the event
        /// is copied. The remainder of the struct is left at default values.
        /// </remarks>
        /// <exception cref="InvalidOperationException"><see cref="stateFormat"/> does not match <see cref="IInputStateTypeInfo.format"/>
        /// of <typeparamref name="TState"/>.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="ptr"/> is <c>default(InputEventPtr)</c>.</exception>
        /// <exception cref="InvalidCastException"><paramref name="ptr"/> does not refer to a StateEvent.</exception>
        /// <seealso cref="GetState{T}()"/>
        public static TState GetState<TState>(InputEventPtr ptr)
            where TState : struct, IInputStateTypeInfo
        {
            return From(ptr)->GetState<TState>();
        }

        public static int GetEventSizeWithPayload<TState>()
            where TState : struct
        {
            return UnsafeUtility.SizeOf<TState>() + InputEvent.kBaseEventSize + sizeof(int);
        }

        /// <summary>
        /// Return the given <see cref="InputEventPtr"/> as a StateEvent pointer.
        /// </summary>
        /// <param name="ptr">A pointer to an input event. The pointer is checked for <c>null</c> and
        /// for whether the type of event it refers to is indeed a StateEvent.</param>
        /// <returns>Pointer <paramref name="ptr"/> converted to a StateEvent pointer.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="ptr"/> is <c>default(InputEventPtr)</c>.</exception>
        /// <exception cref="InvalidCastException"><paramref name="ptr"/> does not refer to a StateEvent.</exception>
        public static StateEvent* From(InputEventPtr ptr)
        {
            if (!ptr.valid)
                throw new ArgumentNullException(nameof(ptr));
            if (!ptr.IsA<StateEvent>())
                throw new InvalidCastException($"Cannot cast event with type '{ptr.type}' into StateEvent");

            return FromUnchecked(ptr);
        }

        internal static StateEvent* FromUnchecked(InputEventPtr ptr)
        {
            return (StateEvent*)ptr.data;
        }

        /// <summary>
        /// Read the current state of <paramref name="device"/> and create a state event from it.
        /// </summary>
        /// <param name="device">Device to grab the state from. Must be a device that has been added to the system.</param>
        /// <param name="eventPtr">Receives a pointer to the newly created state event.</param>
        /// <param name="allocator">Which native allocator to allocate memory for the event from. By default, the buffer is
        /// allocated as temporary memory (<see cref="Allocator.Temp"/>. Note that this means the buffer will not be valid
        /// past the current frame. Use <see cref="Allocator.Persistent"/> if the buffer for the state event is meant to
        /// persist for longer.</param>
        /// <returns>Buffer of unmanaged memory allocated for the event.</returns>
        /// <exception cref="ArgumentException"><paramref name="device"/> has not been added to the system.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
        public static NativeArray<byte> From(InputDevice device, out InputEventPtr eventPtr,  Allocator allocator = Allocator.Temp)
        {
            return From(device, out eventPtr, allocator, useDefaultState: false);
        }

        /// <summary>
        /// Create a state event for the given <paramref name="device"/> and copy the default state of the device
        /// into the event.
        /// </summary>
        /// <param name="device">Device to create a state event for. Must be a device that has been added to the system.</param>
        /// <param name="eventPtr">Receives a pointer to the newly created state event.</param>
        /// <param name="allocator">Which native allocator to allocate memory for the event from. By default, the buffer is
        /// allocated as temporary memory (<see cref="Allocator.Temp"/>. Note that this means the buffer will not be valid
        /// past the current frame. Use <see cref="Allocator.Persistent"/> if the buffer for the state event is meant to
        /// persist for longer.</param>
        /// <returns>Buffer of unmanaged memory allocated for the event.</returns>
        /// <exception cref="ArgumentException"><paramref name="device"/> has not been added to the system.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
        public static NativeArray<byte> FromDefaultStateFor(InputDevice device, out InputEventPtr eventPtr,  Allocator allocator = Allocator.Temp)
        {
            return From(device, out eventPtr, allocator, useDefaultState: true);
        }

        private static NativeArray<byte> From(InputDevice device, out InputEventPtr eventPtr,  Allocator allocator, bool useDefaultState)
        {
            if (device == null)
                throw new ArgumentNullException(nameof(device));
            if (!device.added)
                throw new ArgumentException($"Device '{device}' has not been added to system",
                    nameof(device));

            var stateFormat = device.m_StateBlock.format;
            var stateSize = device.m_StateBlock.alignedSizeInBytes;
            var stateOffset = device.m_StateBlock.byteOffset;
            var statePtr = (byte*)(useDefaultState ? device.defaultStatePtr : device.currentStatePtr) + (int)stateOffset;
            var eventSize = InputEvent.kBaseEventSize + sizeof(int) + stateSize;

            var buffer = new NativeArray<byte>((int)eventSize.AlignToMultipleOf(4), allocator);
            var stateEventPtr = (StateEvent*)buffer.GetUnsafePtr();

            stateEventPtr->baseEvent = new InputEvent(Type, (int)eventSize, device.deviceId, InputRuntime.s_Instance.currentTime);
            stateEventPtr->stateFormat = stateFormat;

            UnsafeUtility.MemCpy(stateEventPtr->state, statePtr, stateSize);

            eventPtr = stateEventPtr->ToEventPtr();
            return buffer;
        }
    }
}