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

namespace UnityEngine.InputSystem
{
    internal partial class InputManager
    {
        // Indices correspond with those in m_Devices.
        internal StateChangeMonitorsForDevice[] m_StateChangeMonitors;
        private InlinedArray<StateChangeMonitorTimeout> m_StateChangeMonitorTimeouts;

        ////TODO: support combining monitors for bitfields
        public void AddStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex, uint groupIndex)
        {
            Debug.Assert(m_DevicesCount > 0);

            var device = control.device;
            var deviceIndex = device.m_DeviceIndex;
            Debug.Assert(deviceIndex != InputDevice.kInvalidDeviceIndex);

            // Allocate/reallocate monitor arrays, if necessary.
            // We lazy-sync it to array of devices.
            if (m_StateChangeMonitors == null)
                m_StateChangeMonitors = new StateChangeMonitorsForDevice[m_DevicesCount];
            else if (m_StateChangeMonitors.Length <= deviceIndex)
                Array.Resize(ref m_StateChangeMonitors, m_DevicesCount);

            // If we have removed monitors
            if (!isProcessingEvents && m_StateChangeMonitors[deviceIndex].needToCompactArrays)
                m_StateChangeMonitors[deviceIndex].CompactArrays();

            // Add record.
            m_StateChangeMonitors[deviceIndex].Add(control, monitor, monitorIndex, groupIndex);
        }

        private void RemoveStateChangeMonitors(InputDevice device)
        {
            if (m_StateChangeMonitors == null)
                return;

            var deviceIndex = device.m_DeviceIndex;
            Debug.Assert(deviceIndex != InputDevice.kInvalidDeviceIndex);

            if (deviceIndex >= m_StateChangeMonitors.Length)
                return;

            m_StateChangeMonitors[deviceIndex].Clear();

            // Clear timeouts pending on any control on the device.
            for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i)
                if (m_StateChangeMonitorTimeouts[i].control?.device == device)
                    m_StateChangeMonitorTimeouts[i] = default;
        }

        public void RemoveStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex)
        {
            if (m_StateChangeMonitors == null)
                return;

            var device = control.device;
            var deviceIndex = device.m_DeviceIndex;

            // Ignore if device has already been removed.
            if (deviceIndex == InputDevice.kInvalidDeviceIndex)
                return;

            // Ignore if there are no state monitors set up for the device.
            if (deviceIndex >= m_StateChangeMonitors.Length)
                return;

            m_StateChangeMonitors[deviceIndex].Remove(monitor, monitorIndex, isProcessingEvents);

            // Remove pending timeouts on the monitor.
            for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i)
                if (m_StateChangeMonitorTimeouts[i].monitor == monitor &&
                    m_StateChangeMonitorTimeouts[i].monitorIndex == monitorIndex)
                    m_StateChangeMonitorTimeouts[i] = default;
        }

        public void AddStateChangeMonitorTimeout(InputControl control, IInputStateChangeMonitor monitor, double time, long monitorIndex, int timerIndex)
        {
            m_StateChangeMonitorTimeouts.Append(
                new StateChangeMonitorTimeout
                {
                    control = control,
                    time = time,
                    monitor = monitor,
                    monitorIndex = monitorIndex,
                    timerIndex = timerIndex,
                });
        }

        public void RemoveStateChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex, int timerIndex)
        {
            var timeoutCount = m_StateChangeMonitorTimeouts.length;
            for (var i = 0; i < timeoutCount; ++i)
            {
                ////REVIEW: can we avoid the repeated array lookups without copying the struct out?
                if (ReferenceEquals(m_StateChangeMonitorTimeouts[i].monitor, monitor)
                    && m_StateChangeMonitorTimeouts[i].monitorIndex == monitorIndex
                    && m_StateChangeMonitorTimeouts[i].timerIndex == timerIndex)
                {
                    m_StateChangeMonitorTimeouts[i] = default;
                    break;
                }
            }
        }

        private void SortStateChangeMonitorsIfNecessary(int deviceIndex)
        {
            if (m_StateChangeMonitors != null && deviceIndex < m_StateChangeMonitors.Length &&
                m_StateChangeMonitors[deviceIndex].needToUpdateOrderingOfMonitors)
                m_StateChangeMonitors[deviceIndex].SortMonitorsByIndex();
        }

        public void SignalStateChangeMonitor(InputControl control, IInputStateChangeMonitor monitor)
        {
            var device = control.device;
            var deviceIndex = device.m_DeviceIndex;

            ref var monitorsForDevice = ref m_StateChangeMonitors[deviceIndex];
            for (var i = 0; i < monitorsForDevice.signalled.length; ++i)
            {
                SortStateChangeMonitorsIfNecessary(i);

                ref var listener = ref monitorsForDevice.listeners[i];
                if (listener.control == control && listener.monitor == monitor)
                    monitorsForDevice.signalled.SetBit(i);
            }
        }

        public unsafe void FireStateChangeNotifications()
        {
            var time = m_Runtime.currentTime;
            var count = Math.Min(m_StateChangeMonitors.LengthSafe(), m_DevicesCount);
            for (var i = 0; i < count; ++i)
                FireStateChangeNotifications(i, time, null);
        }

        // Record for a timeout installed on a state change monitor.
        private struct StateChangeMonitorTimeout
        {
            public InputControl control;
            public double time;
            public IInputStateChangeMonitor monitor;
            public long monitorIndex;
            public int timerIndex;
        }

        // Maps a single control to an action interested in the control. If
        // multiple actions are interested in the same control, we will end up
        // processing the control repeatedly but we assume this is the exception
        // and so optimize for the case where there's only one action going to
        // a control.
        //
        // Split into two structures to keep data needed only when there is an
        // actual value change out of the data we need for doing the scanning.
        internal struct StateChangeMonitorListener
        {
            public InputControl control;
            public IInputStateChangeMonitor monitor;
            public long monitorIndex;
            public uint groupIndex;
        }

        internal struct StateChangeMonitorsForDevice
        {
            public MemoryHelpers.BitRegion[] memoryRegions;
            public StateChangeMonitorListener[] listeners;
            public DynamicBitfield signalled;
            public bool needToUpdateOrderingOfMonitors;
            public bool needToCompactArrays;

            public int count => signalled.length;

            public void Add(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex, uint groupIndex)
            {
                // NOTE: This method must only *append* to arrays. This way we can safely add data while traversing
                //       the arrays in FireStateChangeNotifications. Note that appending *may* mean that the arrays
                //       are switched to larger arrays.

                // Record listener.
                var listenerCount = signalled.length;
                ArrayHelpers.AppendWithCapacity(ref listeners, ref listenerCount,
                    new StateChangeMonitorListener
                    { monitor = monitor, monitorIndex = monitorIndex, groupIndex = groupIndex, control = control });

                // Record memory region.
                ref var controlStateBlock = ref control.m_StateBlock;
                var memoryRegionCount = signalled.length;
                ArrayHelpers.AppendWithCapacity(ref memoryRegions, ref memoryRegionCount,
                    new MemoryHelpers.BitRegion(controlStateBlock.byteOffset - control.device.stateBlock.byteOffset,
                        controlStateBlock.bitOffset, controlStateBlock.sizeInBits));

                signalled.SetLength(signalled.length + 1);

                needToUpdateOrderingOfMonitors = true;
            }

            public void Remove(IInputStateChangeMonitor monitor, long monitorIndex, bool deferRemoval)
            {
                if (listeners == null)
                    return;

                for (var i = 0; i < signalled.length; ++i)
                    if (ReferenceEquals(listeners[i].monitor, monitor) && listeners[i].monitorIndex == monitorIndex)
                    {
                        if (deferRemoval)
                        {
                            listeners[i] = default;
                            memoryRegions[i] = default;
                            signalled.ClearBit(i);
                            needToCompactArrays = true;
                        }
                        else
                        {
                            RemoveAt(i);
                        }

                        break;
                    }
            }

            public void Clear()
            {
                // We don't actually release memory we've potentially allocated but rather just reset
                // our count to zero.
                listeners.Clear(count);
                signalled.SetLength(0);

                needToCompactArrays = false;
            }

            public void CompactArrays()
            {
                for (var i = count - 1; i >= 0; --i)
                {
                    var memoryRegion = memoryRegions[i];
                    if (memoryRegion.sizeInBits != 0)
                        continue;

                    RemoveAt(i);
                }

                needToCompactArrays = false;
            }

            private void RemoveAt(int i)
            {
                var numListeners = count;
                var numMemoryRegions = count;
                listeners.EraseAtWithCapacity(ref numListeners, i);
                memoryRegions.EraseAtWithCapacity(ref numMemoryRegions, i);
                signalled.SetLength(count - 1);
            }

            public void SortMonitorsByIndex()
            {
                // Insertion sort.
                for (var i = 1; i < signalled.length; ++i)
                {
                    for (var j = i; j > 0; --j)
                    {
                        // Sort by complexities only to keep the sort stable
                        // i.e. don't reverse the order of controls which have the same complexity
                        var firstComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j - 1].monitorIndex);
                        var secondComplexity = InputActionState.GetComplexityFromMonitorIndex(listeners[j].monitorIndex);
                        if (firstComplexity >= secondComplexity)
                            break;

                        listeners.SwapElements(j, j - 1);
                        memoryRegions.SwapElements(j, j - 1);

                        // We can ignore the `signalled` array here as we call this method only
                        // when all monitors are in non-signalled state.
                    }
                }

                needToUpdateOrderingOfMonitors = false;
            }
        }

        // NOTE: 'newState' can be a subset of the full state stored at 'oldState'. In this case,
        //       'newStateOffsetInBytes' must give the offset into the full state and 'newStateSizeInBytes' must
        //       give the size of memory slice to be updated.
        private unsafe bool ProcessStateChangeMonitors(int deviceIndex, void* newStateFromEvent, void* oldStateOfDevice, uint newStateSizeInBytes, uint newStateOffsetInBytes)
        {
            if (m_StateChangeMonitors == null)
                return false;

            // We resize the monitor arrays only when someone adds to them so they
            // may be out of sync with the size of m_Devices.
            if (deviceIndex >= m_StateChangeMonitors.Length)
                return false;

            var memoryRegions = m_StateChangeMonitors[deviceIndex].memoryRegions;
            if (memoryRegions == null)
                return false; // No one cares about state changes on this device.

            var numMonitors = m_StateChangeMonitors[deviceIndex].count;
            var signalled = false;
            var signals = m_StateChangeMonitors[deviceIndex].signalled;
            var haveChangedSignalsBitfield = false;

            // For every memory region that overlaps what we got in the event, compare memory contents
            // between the old device state and what's in the event. If the contents different, the
            // respective state monitor signals.
            var newEventMemoryRegion = new MemoryHelpers.BitRegion(newStateOffsetInBytes, 0, newStateSizeInBytes * 8);
            for (var i = 0; i < numMonitors; ++i)
            {
                var memoryRegion = memoryRegions[i];

                // Check if the monitor record has been wiped in the meantime. If so, remove it.
                if (memoryRegion.sizeInBits == 0)
                {
                    ////REVIEW: Do we really care? It is nice that it's predictable this way but hardly a hard requirement
                    // NOTE: We're using EraseAtWithCapacity here rather than EraseAtByMovingTail to preserve
                    //       order which makes the order of callbacks somewhat more predictable.

                    var listenerCount = numMonitors;
                    var memoryRegionCount = numMonitors;
                    m_StateChangeMonitors[deviceIndex].listeners.EraseAtWithCapacity(ref listenerCount, i);
                    memoryRegions.EraseAtWithCapacity(ref memoryRegionCount, i);
                    signals.SetLength(numMonitors - 1);
                    haveChangedSignalsBitfield = true;
                    --numMonitors;
                    --i;
                    continue;
                }

                var overlap = newEventMemoryRegion.Overlap(memoryRegion);
                if (overlap.isEmpty || MemoryHelpers.Compare(oldStateOfDevice, (byte*)newStateFromEvent - newStateOffsetInBytes, overlap))
                    continue;

                signals.SetBit(i);
                haveChangedSignalsBitfield = true;
                signalled = true;
            }

            if (haveChangedSignalsBitfield)
                m_StateChangeMonitors[deviceIndex].signalled = signals;

            m_StateChangeMonitors[deviceIndex].needToCompactArrays = false;

            return signalled;
        }

        internal unsafe void FireStateChangeNotifications(int deviceIndex, double internalTime, InputEvent* eventPtr)
        {
            Debug.Assert(m_StateChangeMonitors != null);
            Debug.Assert(m_StateChangeMonitors.Length > deviceIndex);

            // NOTE: This method must be safe for mutating the state change monitor arrays from *within*
            //       NotifyControlStateChanged()! This includes all monitors for the device being wiped
            //       completely or arbitrary additions and removals having occurred.

            ref var signals = ref m_StateChangeMonitors[deviceIndex].signalled;
            ref var listeners = ref m_StateChangeMonitors[deviceIndex].listeners;
            var time = internalTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;

            // If we don't have an event, gives us as dummy, invalid instance.
            // What matters is that InputEventPtr.valid is false for these.
            var tempEvent = new InputEvent(new FourCC('F', 'A', 'K', 'E'), InputEvent.kBaseEventSize, -1, internalTime);
            if (eventPtr == null)
                eventPtr = (InputEvent*)UnsafeUtility.AddressOf(ref tempEvent);

            // Call IStateChangeMonitor.NotifyControlStateChange for every monitor that is in
            // signalled state.
            eventPtr->handled = false;
            for (var i = 0; i < signals.length; ++i)
            {
                if (!signals.TestBit(i))
                    continue;

                var listener = listeners[i];
                try
                {
                    listener.monitor.NotifyControlStateChanged(listener.control, time, eventPtr,
                        listener.monitorIndex);
                }
                catch (Exception exception)
                {
                    Debug.LogError(
                        $"Exception '{exception.GetType().Name}' thrown from state change monitor '{listener.monitor.GetType().Name}' on '{listener.control}'");
                    Debug.LogException(exception);
                }

                // If the monitor signalled that it has processed the state change, reset all signalled
                // state monitors in the same group. This is what causes "SHIFT+B" to prevent "B" from
                // also triggering.
                if (eventPtr->handled)
                {
                    var groupIndex = listeners[i].groupIndex;
                    for (var n = i + 1; n < signals.length; ++n)
                    {
                        // NOTE: We restrict the preemption logic here to a single monitor. Otherwise,
                        //       we will have to require that group indices are stable *between*
                        //       monitors. Two separate InputActionStates, for example, would have to
                        //       agree on group indices that valid *between* the two states or we end
                        //       up preempting unrelated inputs.
                        //
                        //       Note that this implies there there is *NO* preemption between singleton
                        //       InputActions. This isn't intuitive.
                        if (listeners[n].groupIndex == groupIndex && listeners[n].monitor == listener.monitor)
                            signals.ClearBit(n);
                    }

                    // Need to reset it back to false as we may have more signalled state monitors that
                    // aren't in the same group (i.e. have independent inputs).
                    eventPtr->handled = false;
                }

                signals.ClearBit(i);
            }
        }

        private void ProcessStateChangeMonitorTimeouts()
        {
            if (m_StateChangeMonitorTimeouts.length == 0)
                return;

            // Go through the list and both trigger expired timers and remove any irrelevant
            // ones by compacting the array.
            // NOTE: We do not actually release any memory we may have allocated.
            var currentTime = m_Runtime.currentTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
            var remainingTimeoutCount = 0;
            for (var i = 0; i < m_StateChangeMonitorTimeouts.length; ++i)
            {
                // If we have reset this entry in RemoveStateChangeMonitorTimeouts(),
                // skip over it and let compaction get rid of it.
                if (m_StateChangeMonitorTimeouts[i].control == null)
                    continue;

                var timerExpirationTime = m_StateChangeMonitorTimeouts[i].time;
                if (timerExpirationTime <= currentTime)
                {
                    var timeout = m_StateChangeMonitorTimeouts[i];
                    timeout.monitor.NotifyTimerExpired(timeout.control,
                        currentTime, timeout.monitorIndex, timeout.timerIndex);

                    // Compaction will get rid of the entry.
                }
                else
                {
                    // Rather than repeatedly calling RemoveAt() and thus potentially
                    // moving the same data over and over again, we compact the array
                    // on the fly and move entries in the array down as needed.
                    if (i != remainingTimeoutCount)
                        m_StateChangeMonitorTimeouts[remainingTimeoutCount] = m_StateChangeMonitorTimeouts[i];
                    ++remainingTimeoutCount;
                }
            }

            m_StateChangeMonitorTimeouts.SetLength(remainingTimeoutCount);
        }
    }
}