using System.Diagnostics;
using System.Threading;

namespace OpenHardwareMonitor.Hardware.Motherboard.Lpc.EC;

/// <summary>
/// An unsafe but universal implementation for the ACPI Embedded Controller IO interface for Windows
/// </summary>
/// <remarks>
/// It is unsafe because of possible race condition between this application and the PC firmware when
/// writing to the EC registers. For a safe approach ACPI/WMI methods have to be used, but those are
/// different for each motherboard model.
/// </remarks>
public class WindowsEmbeddedControllerIO : IEmbeddedControllerIO
{
    private const int FailuresBeforeSkip = 20;
    private const int MaxRetries = 5;

    // implementation
    private const int WaitSpins = 50;
    private bool _disposed;

    private int _waitReadFailures;

    public WindowsEmbeddedControllerIO()
    {
        if (!Mutexes.WaitEc(10))
        {
            throw new BusMutexLockingFailedException();
        }
    }

    public void Read(ushort[] registers, byte[] data)
    {
        Trace.Assert(registers.Length <= data.Length,
                     "data buffer length has to be greater or equal to the registers array length");

        byte bank = 0;
        byte prevBank = SwitchBank(bank);

        // oops... somebody else is working with the EC too
        Trace.WriteLineIf(prevBank != 0, "Concurrent access to the ACPI EC detected.\nRace condition possible.");

        // read registers minimizing bank switches.
        for (int i = 0; i < registers.Length; i++)
        {
            byte regBank = (byte)(registers[i] >> 8);
            byte regIndex = (byte)(registers[i] & 0xFF);
            // registers are sorted by bank
            if (regBank > bank)
            {
                bank = SwitchBank(regBank);
            }
            data[i] = ReadByte(regIndex);
        }

        SwitchBank(prevBank);
    }

    private byte ReadByte(byte register) => ReadLoop<byte>(register, ReadByteOp);
    private void WriteByte(byte register, byte value) => WriteLoop(register, value, WriteByteOp);

    public void Dispose()
    {
        if (!_disposed)
        {
            _disposed = true;
            Mutexes.ReleaseEc();
        }
    }

    private byte SwitchBank(byte bank)
    {
        byte previous = ReadByte(0xFF);
        WriteByte(0xFF, bank);
        return previous;
    }

    private static TResult ReadLoop<TResult>(byte register, ReadOp<TResult> op) where TResult : new()
    {
        TResult result = new();

        for (int i = 0; i < MaxRetries; i++)
        {
            if (op(register, out result))
            {
                return result;
            }
        }

        return result;
    }

    private static void WriteLoop<TValue>(byte register, TValue value, WriteOp<TValue> op)
    {
        for (int i = 0; i < MaxRetries; i++)
        {
            if (op(register, value))
            {
                return;
            }
        }
    }

    private static bool WaitForStatus(Status status, bool isSet)
    {
        for (int i = 0; i < WaitSpins; i++)
        {
            byte value = ReadIoPort(Port.Command);

            if (((byte)status & (!isSet ? value : (byte)~value)) == 0)
            {
                return true;
            }

            Thread.Sleep(1);
        }

        return false;
    }

    private bool WaitRead()
    {
        if (_waitReadFailures > FailuresBeforeSkip)
        {
            return true;
        }

        // Try OBF with reduced timeout
        for (int i = 0; i < MaxRetries; i++)
        {
            byte status = ReadIoPort(Port.Command);
            if ((status & (byte)Status.OutputBufferFull) != 0)
            {
                _waitReadFailures = 0;
                return true;
            }

            Thread.Sleep(1);
        }

        // ASUS workaround: Wait for IBF to clear instead of OBF
        // Testing on Z170 Pro Gaming shows IBF clears in 1-3ms when data is ready
        for (int i = 0; i < WaitSpins; i++)
        {
            byte status = ReadIoPort(Port.Command);
            if ((status & (byte)Status.InputBufferFull) == 0)
            {
                _waitReadFailures = 0;
                return true;
            }

            Thread.Sleep(1);
        }

        _waitReadFailures++;
        return false;
    }

    private bool WaitWrite()
    {
        return WaitForStatus(Status.InputBufferFull, false);
    }

    private static byte ReadIoPort(Port port) => Ring0.ReadIoPort((byte)port);

    private static void WriteIoPort(Port port, byte datum) => Ring0.WriteIoPort((byte)port, datum);

    protected bool ReadByteOp(byte register, out byte value)
    {
        if (WaitWrite())
        {
            WriteIoPort(Port.Command, (byte)Command.Read);

            if (WaitWrite())
            {
                WriteIoPort(Port.Data, register);

                if (WaitWrite() && WaitRead())
                {
                    value = ReadIoPort(Port.Data);
                    return true;
                }
            }
        }

        value = 0;
        return false;
    }

    protected bool WriteByteOp(byte register, byte value)
    {
        if (WaitWrite())
        {
            WriteIoPort(Port.Command, (byte)Command.Write);
            if (WaitWrite())
            {
                WriteIoPort(Port.Data, register);
                if (WaitWrite())
                {
                    WriteIoPort(Port.Data, value);
                    return true;
                }
            }
        }

        return false;
    }

    public class BusMutexLockingFailedException : EmbeddedController.IOException
    {
        public BusMutexLockingFailedException()
            : base("could not lock ISA bus mutex")
        { }
    }

    private delegate bool ReadOp<TParam>(byte register, out TParam p);

    private delegate bool WriteOp<in TParam>(byte register, TParam p);

    // see the ACPI specification chapter 12
    private enum Port : byte
    {
        Command = 0x66,
        Data = 0x62
    }

    private enum Command : byte
    {
        Read = 0x80, // RD_EC
        Write = 0x81, // WR_EC
        BurstEnable = 0x82, // BE_EC
        BurstDisable = 0x83, // BD_EC
        Query = 0x84 // QR_EC
    }

    private enum Status : byte
    {
        OutputBufferFull = 0x01, // EC_OBF
        InputBufferFull = 0x02, // EC_IBF
        Command = 0x08, // CMD
        BurstMode = 0x10, // BURST
        SciEventPending = 0x20, // SCI_EVT
        SmiEventPending = 0x40 // SMI_EVT
    }
}
