// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Threading;

namespace LibreHardwareMonitor.Hardware.Motherboard.Lpc;

internal class Nct677X : ISuperIO
{
    private readonly struct TemperatureSourceData
    {
        public TemperatureSourceData(Enum source, ushort register, ushort halfRegister = 0, int halfBit = -1, ushort sourceRegister = 0, ushort? alternateRegister = null)
        {
            Source = source;
            Register = register;
            HalfRegister = halfRegister;
            HalfBit = halfBit;
            SourceRegister = sourceRegister;
            AlternateRegister = alternateRegister;
        }
        public readonly Enum Source;
        public readonly ushort Register;
        public readonly ushort HalfRegister;
        public readonly int HalfBit;
        public readonly ushort SourceRegister;
        public readonly ushort? AlternateRegister;
    }

    private readonly ushort[] _fanCountRegister;
    private readonly ushort[] _fanRpmRegister;
    private readonly byte[] _initialFanControlMode = new byte[7];
    private readonly byte[] _initialFanPwmCommand = new byte[7];
    private readonly bool _isNuvotonVendor;
    private readonly LpcPort _lpcPort;
    private readonly int _maxFanCount;
    private readonly int _minFanCount;
    private readonly int _minFanRpm;
    private readonly ushort _port;
    private readonly bool[] _restoreDefaultFanControlRequired = new bool[7];
    private readonly byte _revision;
    private readonly TemperatureSourceData[] _temperaturesSource;
    private readonly ushort _vBatMonitorControlRegister;
    private readonly ushort[] _voltageRegisters;
    private readonly ushort _voltageVBatRegister;

    public Nct677X(Chip chip, byte revision, ushort port, LpcPort lpcPort)
    {
        Chip = chip;
        _revision = revision;
        _port = port;
        _lpcPort = lpcPort;

        if (chip == Chip.NCT610XD)
        {
            VENDOR_ID_HIGH_REGISTER = 0x80FE;
            VENDOR_ID_LOW_REGISTER = 0x00FE;

            FAN_PWM_OUT_REG = new ushort[] { 0x04A, 0x04B, 0x04C };
            FAN_PWM_COMMAND_REG = new ushort[] { 0x119, 0x129, 0x139 };
            FAN_CONTROL_MODE_REG = new ushort[] { 0x113, 0x123, 0x133 };

            _vBatMonitorControlRegister = 0x0318;
        }
        else if (chip is Chip.NCT6683D or Chip.NCT6686D or Chip.NCT6687D)
        {
            FAN_PWM_OUT_REG = new ushort[] { 0x160, 0x161, 0x162, 0x163, 0x164, 0x165, 0x166, 0x167 };
            FAN_PWM_COMMAND_REG = new ushort[] { 0xA28, 0xA29, 0xA2A, 0xA2B, 0xA2C, 0xA2D, 0xA2E, 0xA2F };
            FAN_CONTROL_MODE_REG = new ushort[] { 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00 };
            FAN_PWM_REQUEST_REG = new ushort[] { 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01 };
        }
        else
        {
            VENDOR_ID_HIGH_REGISTER = 0x804F;
            VENDOR_ID_LOW_REGISTER = 0x004F;

            FAN_PWM_OUT_REG = chip is Chip.NCT6797D or Chip.NCT6798D or Chip.NCT6799D
                ? new ushort[] { 0x001, 0x003, 0x011, 0x013, 0x015, 0xA09, 0xB09 }
                : new ushort[] { 0x001, 0x003, 0x011, 0x013, 0x015, 0x017, 0x029 };

            FAN_PWM_COMMAND_REG = new ushort[] { 0x109, 0x209, 0x309, 0x809, 0x909, 0xA09, 0xB09 };
            FAN_CONTROL_MODE_REG = new ushort[] { 0x102, 0x202, 0x302, 0x802, 0x902, 0xA02, 0xB02 };

            _vBatMonitorControlRegister = 0x005D;
        }

        _isNuvotonVendor = IsNuvotonVendor();

        if (!_isNuvotonVendor)
            return;

        switch (chip)
        {
            case Chip.NCT6771F:
            case Chip.NCT6776F:
                if (chip == Chip.NCT6771F)
                {
                    Fans = new float?[4];

                    // min value RPM value with 16-bit fan counter
                    _minFanRpm = (int)(1.35e6 / 0xFFFF);
                }
                else
                {
                    Fans = new float?[5];

                    // min value RPM value with 13-bit fan counter
                    _minFanRpm = (int)(1.35e6 / 0x1FFF);
                }

                _fanRpmRegister = new ushort[5];
                for (int i = 0; i < _fanRpmRegister.Length; i++)
                    _fanRpmRegister[i] = (ushort)(0x656 + (i << 1));

                Controls = new float?[3];

                Voltages = new float?[9];
                _voltageRegisters = new ushort[] { 0x020, 0x021, 0x022, 0x023, 0x024, 0x025, 0x026, 0x550, 0x551 };
                _voltageVBatRegister = 0x551;
                _temperaturesSource = new TemperatureSourceData[]
                {
                    new(chip == Chip.NCT6771F ?  SourceNct6771F.PECI_0 : SourceNct6776F.PECI_0, 0x027, 0, -1, 0x621),
                    new(chip == Chip.NCT6771F ?  SourceNct6771F.CPUTIN : SourceNct6776F.CPUTIN, 0x073, 0x074, 7, 0x100),
                    new(chip == Chip.NCT6771F ?  SourceNct6771F.AUXTIN : SourceNct6776F.AUXTIN, 0x075, 0x076, 7, 0x200),
                    new(chip == Chip.NCT6771F ?  SourceNct6771F.SYSTIN : SourceNct6776F.SYSTIN, 0x077, 0x078, 7, 0x300),
                    new(null, 0x150, 0x151, 7, 0x622),
                    new(null, 0x250, 0x251, 7, 0x623),
                    new(null, 0x62B, 0x62E, 0, 0x624),
                    new(null, 0x62C, 0x62E, 1, 0x625),
                    new(null, 0x62D, 0x62E, 2, 0x626)
                };

                Temperatures = new float?[4];
                break;

            case Chip.NCT6779D: // 15 voltages
            case Chip.NCT6791D: // 15 voltages
            case Chip.NCT6792D:
            case Chip.NCT6792DA:
            case Chip.NCT6793D: // 14 voltages
            case Chip.NCT6795D:
            case Chip.NCT6796D: // 16 voltages
            case Chip.NCT6796DR: // 16 voltages
            case Chip.NCT6797D:
            case Chip.NCT6798D:
            case Chip.NCT6799D:
                switch (chip)
                {
                    case Chip.NCT6779D:
                        Fans = new float?[5];
                        Controls = new float?[5];
                        break;

                    case Chip.NCT6796DR:
                    case Chip.NCT6797D:
                    case Chip.NCT6798D:
                    case Chip.NCT6799D:
                        Fans = new float?[7];
                        Controls = new float?[7];
                        break;

                    default:
                        Fans = new float?[6];
                        Controls = new float?[6];
                        break;
                }

                _fanCountRegister = new ushort[] { 0x4B0, 0x4B2, 0x4B4, 0x4B6, 0x4B8, 0x4BA, 0x4CC };

                // max value for 13-bit fan counter
                _maxFanCount = 0x1FFF;

                // min value that could be transferred to 16-bit RPM registers
                _minFanCount = 0x15;

                Voltages = new float?[16];
                _voltageRegisters = new ushort[] { 0x480, 0x481, 0x482, 0x483, 0x484, 0x485, 0x486, 0x487, 0x488, 0x489, 0x48A, 0x48B, 0x48C, 0x48D, 0x48E, 0x48F };
                _voltageVBatRegister = 0x488;
                var temperaturesSources = new List<TemperatureSourceData>();

                switch (chip)
                {
                    case Chip.NCT6796D:
                    case Chip.NCT6796DR:
                    case Chip.NCT6797D:
                    case Chip.NCT6798D:
                    case Chip.NCT6799D:
                        temperaturesSources.AddRange(new TemperatureSourceData[]
                        {
                            new(SourceNct67Xxd.PECI_0, 0x073, 0x074, 7, 0x100),
                            new(SourceNct67Xxd.CPUTIN, 0x075, 0x076, 7, 0x200, 0x491),
                            new(SourceNct67Xxd.SYSTIN, 0x077, 0x078, 7, 0x300, 0x490),
                            new(SourceNct67Xxd.AUXTIN0, 0x079, 0x07A, 7, 0x800, 0x492),
                            new(SourceNct67Xxd.AUXTIN1, 0x07B, 0x07C, 7, 0x900, 0x493),
                            new(SourceNct67Xxd.AUXTIN2, 0x07D, 0x07E, 7, 0xA00, 0x494),
                            new(SourceNct67Xxd.AUXTIN3, 0x4A0, 0x49E, 6, 0xB00, 0x495),
                            new(SourceNct67Xxd.AUXTIN4, 0x027, 0, -1, 0x621),
                            new(SourceNct67Xxd.TSENSOR, 0x4A2, 0x4A1, 7, 0xC00, 0x496),
                            new(SourceNct67Xxd.SMBUSMASTER0, 0x150, 0x151, 7, 0x622),
                            new(SourceNct67Xxd.SMBUSMASTER1, 0x670, 0, -1, 0xC26),
                            new(SourceNct67Xxd.PECI_1, 0x672, 0, -1, 0xC27),
                            new(SourceNct67Xxd.PCH_CHIP_CPU_MAX_TEMP, 0x674, 0, -1, 0xC28, 0x400),
                            new(SourceNct67Xxd.PCH_CHIP_TEMP, 0x676, 0, -1, 0xC29, 0x401),
                            new(SourceNct67Xxd.PCH_CPU_TEMP,  0x678, 0, -1, 0xC2A, 0x402),
                            new(SourceNct67Xxd.PCH_MCH_TEMP, 0x67A, 0, -1, 0xC2B, 0x404),
                            new(SourceNct67Xxd.AGENT0_DIMM0, 0),
                            new(SourceNct67Xxd.AGENT0_DIMM1, 0),
                            new(SourceNct67Xxd.AGENT1_DIMM0, 0),
                            new(SourceNct67Xxd.AGENT1_DIMM1, 0),
                            new(SourceNct67Xxd.BYTE_TEMP0, 0),
                            new(SourceNct67Xxd.BYTE_TEMP1, 0),
                            new(SourceNct67Xxd.PECI_0_CAL, 0),
                            new(SourceNct67Xxd.PECI_1_CAL, 0),
                            new(SourceNct67Xxd.VIRTUAL_TEMP, 0)
                        });
                        break;

                    default:
                        temperaturesSources.AddRange(new TemperatureSourceData[]
                        {
                            new(SourceNct67Xxd.PECI_0, 0x027, 0, -1, 0x621),
                            new(SourceNct67Xxd.CPUTIN, 0x073, 0x074, 7, 0x100, 0x491),
                            new(SourceNct67Xxd.SYSTIN, 0x075, 0x076, 7, 0x200, 0x490),
                            new(SourceNct67Xxd.AUXTIN0, 0x077, 0x078, 7, 0x300, 0x492),
                            new(SourceNct67Xxd.AUXTIN1, 0x079, 0x07A, 7, 0x800, 0x493),
                            new(SourceNct67Xxd.AUXTIN2, 0x07B, 0x07C, 7, 0x900, 0x494),
                            new(SourceNct67Xxd.AUXTIN3, 0x150, 0x151, 7, 0x622, 0x495)
                        });
                        break;
                }

                _temperaturesSource = temperaturesSources.ToArray();
                Temperatures = new float?[_temperaturesSource.Length];
                break;

            case Chip.NCT610XD:
                Fans = new float?[3];
                Controls = new float?[3];

                _fanRpmRegister = new ushort[3];
                for (int i = 0; i < _fanRpmRegister.Length; i++)
                    _fanRpmRegister[i] = (ushort)(0x030 + (i << 1));

                // min value RPM value with 13-bit fan counter
                _minFanRpm = (int)(1.35e6 / 0x1FFF);

                Voltages = new float?[9];
                _voltageRegisters = new ushort[] { 0x300, 0x301, 0x302, 0x303, 0x304, 0x305, 0x307, 0x308, 0x309 };
                _voltageVBatRegister = 0x308;
                Temperatures = new float?[4];
                _temperaturesSource = new TemperatureSourceData[] {
                    new(SourceNct610X.PECI_0, 0x027, 0, -1, 0x621),
                    new(SourceNct610X.SYSTIN, 0x018, 0x01B, 7, 0x100, 0x018),
                    new(SourceNct610X.CPUTIN, 0x019, 0x11B, 7, 0x200, 0x019),
                    new(SourceNct610X.AUXTIN, 0x01A, 0x21B, 7, 0x300, 0x01A)
                };
                break;

            case Chip.NCT6683D:
            case Chip.NCT6686D:
            case Chip.NCT6687D:
                Fans = new float?[8];
                Controls = new float?[8];
                Voltages = new float?[14];
                Temperatures = new float?[7];

                // CPU
                // System
                // MOS
                // PCH
                // CPU Socket
                // PCIE_1
                // M2_1
                _temperaturesSource = new TemperatureSourceData[] {
                    new(null, 0x100),
                    new(null, 0x102),
                    new(null, 0x104),
                    new(null, 0x106),
                    new(null, 0x108),
                    new(null, 0x10A),
                    new(null, 0x10C)
                };

                // VIN0 +12V
                // VIN1 +5V
                // VIN2 VCore
                // VIN3 SIO
                // VIN4 DRAM
                // VIN5 CPU IO
                // VIN6 CPU SA
                // VIN7 SIO
                // 3VCC I/O +3.3
                // SIO VTT
                // SIO VREF
                // SIO VSB
                // SIO AVSB
                // SIO VBAT
                _voltageRegisters = new ushort[] { 0x120, 0x122, 0x124, 0x126, 0x128, 0x12A, 0x12C, 0x12E, 0x130, 0x13A, 0x13E, 0x136, 0x138, 0x13C };

                // CPU Fan
                // PUMP Fan
                // SYS Fan 1
                // SYS Fan 2
                // SYS Fan 3
                // SYS Fan 4
                // SYS Fan 5
                // SYS Fan 6
                _fanRpmRegister = new ushort[] { 0x140, 0x142, 0x144, 0x146, 0x148, 0x14A, 0x14C, 0x14E };

                _restoreDefaultFanControlRequired = new bool[_fanRpmRegister.Length];
                _initialFanControlMode = new byte[_fanRpmRegister.Length];
                _initialFanPwmCommand = new byte[_fanRpmRegister.Length];

                // initialize
                const ushort initRegister = 0x180;
                byte data = ReadByte(initRegister);
                if ((data & 0x80) == 0)
                {
                    WriteByte(initRegister, (byte)(data | 0x80));
                }

                // enable SIO voltage
                WriteByte(0x1BB, 0x61);
                WriteByte(0x1BC, 0x62);
                WriteByte(0x1BD, 0x63);
                WriteByte(0x1BE, 0x64);
                WriteByte(0x1BF, 0x65);
                break;
        }
    }

    public Chip Chip { get; }

    public float?[] Controls { get; } = Array.Empty<float?>();

    public float?[] Fans { get; } = Array.Empty<float?>();

    public float?[] Temperatures { get; } = Array.Empty<float?>();

    public float?[] Voltages { get; } = Array.Empty<float?>();

    public byte? ReadGpio(int index)
    {
        return null;
    }

    public void WriteGpio(int index, byte value)
    { }

    public void SetControl(int index, byte? value)
    {
        if (!_isNuvotonVendor)
            return;

        if (index < 0 || index >= Controls.Length)
            throw new ArgumentOutOfRangeException(nameof(index));

        if (!Mutexes.WaitIsaBus(10))
            return;

        if (value.HasValue)
        {
            SaveDefaultFanControl(index);

            if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
            {
                // set manual mode
                WriteByte(FAN_CONTROL_MODE_REG[index], 0);

                // set output value
                WriteByte(FAN_PWM_COMMAND_REG[index], value.Value);
            }
            else
            {
                // Manual mode, bit(1 : set, 0 : unset)
                // bit 0 : CPU Fan
                // bit 1 : PUMP Fan
                // bit 2 : SYS Fan 1
                // bit 3 : SYS Fan 2
                // bit 4 : SYS Fan 3
                // bit 5 : SYS Fan 4
                // bit 6 : SYS Fan 5
                // bit 7 : SYS Fan 6

                byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
                byte bitMask = (byte)(0x01 << index);
                mode = (byte)(mode | bitMask);
                WriteByte(FAN_CONTROL_MODE_REG[index], mode);

                WriteByte(FAN_PWM_REQUEST_REG[index], 0x80);
                Thread.Sleep(50);

                WriteByte(FAN_PWM_COMMAND_REG[index], value.Value);
                WriteByte(FAN_PWM_REQUEST_REG[index], 0x40);
                Thread.Sleep(50);
            }
        }
        else
        {
            RestoreDefaultFanControl(index);
        }

        Mutexes.ReleaseIsaBus();
    }

    public void Update()
    {
        if (!_isNuvotonVendor)
            return;

        if (!Mutexes.WaitIsaBus(10))
            return;

        DisableIOSpaceLock();

        for (int i = 0; i < Voltages.Length; i++)
        {
            if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
            {
                float value = 0.008f * ReadByte(_voltageRegisters[i]);
                bool valid = value > 0;

                // check if battery voltage monitor is enabled
                if (valid && _voltageRegisters[i] == _voltageVBatRegister)
                    valid = (ReadByte(_vBatMonitorControlRegister) & 0x01) > 0;

                Voltages[i] = valid ? value : null;
            }
            else
            {
                float value = 0.001f * ((16 * ReadByte(_voltageRegisters[i])) + (ReadByte((ushort)(_voltageRegisters[i] + 1)) >> 4));

                Voltages[i] = i switch
                {
                    // 12V
                    0 => value * 12.0f,
                    // 5V
                    1 => value * 5.0f,
                    // DRAM
                    4 => value * 2.0f,
                    _ => value
                };
            }
        }

        System.Diagnostics.Debug.WriteLine("Updating temperatures.");
        long temperatureSourceMask = 0;
        for (int i = 0; i < _temperaturesSource.Length; i++)
        {
            TemperatureSourceData ts = _temperaturesSource[i];
            int value;
            SourceNct67Xxd source;
            float? temperature;

            switch (Chip)
            {
                case Chip.NCT6687D:
                case Chip.NCT6686D:
                case Chip.NCT6683D:
                    value = (sbyte)ReadByte(ts.Register);
                    int half = (ReadByte((ushort)(ts.Register + 1)) >> 7) & 0x1;
                    Temperatures[i] = value + (0.5f * half);
                    break;

                case Chip.NCT6796D:
                case Chip.NCT6796DR:
                case Chip.NCT6797D:
                case Chip.NCT6798D:
                case Chip.NCT6799D:
                    if (_temperaturesSource[i].Register == 0)
                    {
                        System.Diagnostics.Debug.WriteLine("Temperature register {0} skipped, address 0.", i);
                        continue;
                    }

                    value = (sbyte)ReadByte(_temperaturesSource[i].Register) << 1;
                    System.Diagnostics.Debug.WriteLine("Temperature register {0} at 0x{1:X3} value (integer): {2}/2", i, ts.Register, value);
                    if (_temperaturesSource[i].HalfBit > 0)
                    {
                        value |= (ReadByte(_temperaturesSource[i].HalfRegister) >> ts.HalfBit) & 0x1;
                        System.Diagnostics.Debug.WriteLine("Temperature register {0} value updated from 0x{1:X3} (fractional): {2}/2", i, ts.HalfRegister, value);
                    }

                    if (ts.SourceRegister > 0)
                    {
                        source = (SourceNct67Xxd)(ReadByte(ts.SourceRegister) & 0x1F);
                        System.Diagnostics.Debug.WriteLine("Temperature register {0} source at 0x{1:X3}: {2:G} ({2:D})", i, ts.SourceRegister, source);
                    }
                    else
                    {
                        source = (SourceNct67Xxd)ts.Source;
                        System.Diagnostics.Debug.WriteLine("Temperature register {0} source register is 0, source set to: {1:G} ({1:D})", i, source);
                    }

                    // Skip reading when already filled, because later values are without fractional
                    if ((temperatureSourceMask & (1L << (byte)source)) > 0)
                    {
                        System.Diagnostics.Debug.WriteLine("Temperature register {0} discarded, because source seen before.", i);
                        continue;
                    }

                    temperature = 0.5f * value;
                    System.Diagnostics.Debug.WriteLine("Temperature register {0} final temperature: {1}.", i, temperature);
                    if (temperature is > 125 or < -55)
                    {
                        temperature = null;
                        System.Diagnostics.Debug.WriteLine("Temperature register {0} discarded: Out of range.", i);
                    }
                    else
                    {
                        temperatureSourceMask |= 1L << (byte)source;
                        System.Diagnostics.Debug.WriteLine("Temperature register {0} accepted.", i);
                    }

                    for (int j = 0; j < Temperatures.Length; j++)
                    {
                        if ((SourceNct67Xxd)_temperaturesSource[j].Source == source)
                        {
                            Temperatures[j] = temperature;
                            System.Diagnostics.Debug.WriteLine("Temperature register {0}, value from source {1:G} ({1:D}), written at position {2}.", i, _temperaturesSource[j].Source, j);
                        }
                    }
                    break;

                default:
                    value = (sbyte)ReadByte(ts.Register) << 1;
                    if (ts.HalfBit > 0)
                    {
                        value |= (ReadByte(ts.HalfRegister) >> ts.HalfBit) & 0x1;
                    }

                    source = (SourceNct67Xxd)ReadByte(ts.SourceRegister);
                    temperatureSourceMask |= 1L << (byte)source;

                    temperature = 0.5f * value;
                    if (temperature is > 125 or < -55)
                        temperature = null;

                    for (int j = 0; j < Temperatures.Length; j++)
                    {
                        if ((SourceNct67Xxd)_temperaturesSource[j].Source == source)
                            Temperatures[j] = temperature;
                    }
                    break;
            }
        }

        for (int i = 0; i < _temperaturesSource.Length; i++)
        {
            TemperatureSourceData ts = _temperaturesSource[i];
            if (!ts.AlternateRegister.HasValue)
            {
                System.Diagnostics.Debug.WriteLine("Alternate temperature register for temperature {0}, {1:G} ({1:D}), skipped, because address is null.", i, ts.Source);
                continue;
            }

            if ((temperatureSourceMask & (1L << (byte)(SourceNct67Xxd)ts.Source)) > 0)
            {
                System.Diagnostics.Debug.WriteLine("Alternate temperature register for temperature {0}, {1:G} ({1:D}), at 0x{2:X3} skipped, because value already set.", i, ts.Source, ts.AlternateRegister.Value);
                continue;
            }

            float? temperature = (sbyte)ReadByte(ts.AlternateRegister.Value);
            System.Diagnostics.Debug.WriteLine("Alternate temperature register for temperature {0}, {1:G} ({1:D}), at 0x{2:X3} final temperature: {3}.", i, ts.Source, ts.AlternateRegister.Value, temperature);

            if (temperature is > 125 or <= 0)
            {
                temperature = null;
                System.Diagnostics.Debug.WriteLine("Alternate Temperature register for temperature {0}, {1:G} ({1:D}), discarded: Out of range.", i, ts.Source);
            }

            Temperatures[i] = temperature;
        }

        for (int i = 0; i < Fans.Length; i++)
        {
            if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
            {
                if (_fanCountRegister != null)
                {
                    byte high = ReadByte(_fanCountRegister[i]);
                    byte low = ReadByte((ushort)(_fanCountRegister[i] + 1));

                    int count = (high << 5) | (low & 0x1F);
                    if (count < _maxFanCount)
                    {
                        if (count >= _minFanCount)
                        {
                            Fans[i] = 1.35e6f / count;
                        }
                        else
                        {
                            Fans[i] = null;
                        }
                    }
                    else
                    {
                        Fans[i] = 0;
                    }
                }
                else
                {
                    byte high = ReadByte(_fanRpmRegister[i]);
                    byte low = ReadByte((ushort)(_fanRpmRegister[i] + 1));
                    int value = (high << 8) | low;

                    Fans[i] = value > _minFanRpm ? value : 0;
                }
            }
            else
            {
                Fans[i] = (ReadByte(_fanRpmRegister[i]) << 8) | ReadByte((ushort)(_fanRpmRegister[i] + 1));
            }
        }

        for (int i = 0; i < Controls.Length; i++)
        {
            if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
            {
                int value = ReadByte(FAN_PWM_OUT_REG[i]);
                Controls[i] = value / 2.55f;
            }
            else
            {
                int value = ReadByte(FAN_PWM_OUT_REG[i]);
                Controls[i] = (float)Math.Round(value / 2.55f);
            }
        }

        Mutexes.ReleaseIsaBus();
    }

    public string GetReport()
    {
        StringBuilder r = new();

        r.AppendLine("LPC " + GetType().Name);
        r.AppendLine();
        r.Append("Chip Id: 0x");
        r.AppendLine(Chip.ToString("X"));
        r.Append("Chip Revision: 0x");
        r.AppendLine(_revision.ToString("X", CultureInfo.InvariantCulture));
        r.Append("Base Address: 0x");
        r.AppendLine(_port.ToString("X4", CultureInfo.InvariantCulture));
        r.AppendLine();

        if (!Mutexes.WaitIsaBus(100))
            return r.ToString();

        ushort[] addresses =
        {
            0x000,
            0x010,
            0x020,
            0x030,
            0x040,
            0x050,
            0x060,
            0x070,
            0x0F0,
            0x100,
            0x110,
            0x120,
            0x130,
            0x140,
            0x150,
            0x200,
            0x210,
            0x220,
            0x230,
            0x240,
            0x250,
            0x260,
            0x300,
            0x320,
            0x330,
            0x340,
            0x360,
            0x400,
            0x410,
            0x420,
            0x440,
            0x450,
            0x460,
            0x480,
            0x490,
            0x4B0,
            0x4C0,
            0x4F0,
            0x500,
            0x550,
            0x560,
            0x600,
            0x610,
            0x620,
            0x630,
            0x640,
            0x650,
            0x660,
            0x670,
            0x700,
            0x710,
            0x720,
            0x730,
            0x800,
            0x820,
            0x830,
            0x840,
            0x900,
            0x920,
            0x930,
            0x940,
            0x960,
            0xA00,
            0xA10,
            0xA20,
            0xA30,
            0xA40,
            0xA50,
            0xA60,
            0xA70,
            0xB00,
            0xB10,
            0xB20,
            0xB30,
            0xB50,
            0xB60,
            0xB70,
            0xC00,
            0xC10,
            0xC20,
            0xC30,
            0xC50,
            0xC60,
            0xC70,
            0xD00,
            0xD10,
            0xD20,
            0xD30,
            0xD50,
            0xD60,
            0xE00,
            0xE10,
            0xE20,
            0xE30,
            0xF00,
            0xF10,
            0xF20,
            0xF30,
            0x8040,
            0x80F0
        };

        r.AppendLine("Hardware Monitor Registers");
        r.AppendLine();
        r.AppendLine("        00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F");
        r.AppendLine();

        if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
        {
            foreach (ushort address in addresses)
            {
                r.Append(" ");
                r.Append(address.ToString("X4", CultureInfo.InvariantCulture));
                r.Append("  ");
                for (ushort j = 0; j <= 0xF; j++)
                {
                    r.Append(" ");
                    r.Append(ReadByte((ushort)(address | j)).ToString("X2", CultureInfo.InvariantCulture));
                }

                r.AppendLine();
            }
        }
        else
        {
            for (int i = 0; i <= 0xFF; i++)
            {
                r.Append(" ");
                r.Append((i << 4).ToString("X4", CultureInfo.InvariantCulture));
                r.Append("  ");
                for (int j = 0; j <= 0xF; j++)
                {
                    ushort address = (ushort)(i << 4 | j);
                    r.Append(" ");
                    r.Append(ReadByte(address).ToString("X2", CultureInfo.InvariantCulture));
                }

                r.AppendLine();
            }
        }

        r.AppendLine();

        Mutexes.ReleaseIsaBus();

        return r.ToString();
    }

    private byte ReadByte(ushort address)
    {
        if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
        {
            byte bank = (byte)(address >> 8);
            byte register = (byte)(address & 0xFF);
            Ring0.WriteIoPort(_port + ADDRESS_REGISTER_OFFSET, BANK_SELECT_REGISTER);
            Ring0.WriteIoPort(_port + DATA_REGISTER_OFFSET, bank);
            Ring0.WriteIoPort(_port + ADDRESS_REGISTER_OFFSET, register);
            return Ring0.ReadIoPort(_port + DATA_REGISTER_OFFSET);
        }

        byte page = (byte)(address >> 8);
        byte index = (byte)(address & 0xFF);

        //wait for access, access == EC_SPACE_PAGE_SELECT
        //timeout: after 500ms, abort and force access
        byte access;

        DateTime timeout = DateTime.UtcNow.AddMilliseconds(500);
        while (true)
        {
            access = Ring0.ReadIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET);
            if (access == EC_SPACE_PAGE_SELECT || DateTime.UtcNow > timeout)
                break;

            System.Threading.Thread.Sleep(1);
        }

        if (access != EC_SPACE_PAGE_SELECT)
        {
            // Failed to gain access: force register access
            Ring0.WriteIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET, EC_SPACE_PAGE_SELECT);
        }

        Ring0.WriteIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET, page);
        Ring0.WriteIoPort(_port + EC_SPACE_INDEX_REGISTER_OFFSET, index);
        byte result = Ring0.ReadIoPort(_port + EC_SPACE_DATA_REGISTER_OFFSET);

        //free access for other instances
        Ring0.WriteIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET, EC_SPACE_PAGE_SELECT);

        return result;
    }

    private void WriteByte(ushort address, byte value)
    {
        if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
        {
            byte bank = (byte)(address >> 8);
            byte register = (byte)(address & 0xFF);
            Ring0.WriteIoPort(_port + ADDRESS_REGISTER_OFFSET, BANK_SELECT_REGISTER);
            Ring0.WriteIoPort(_port + DATA_REGISTER_OFFSET, bank);
            Ring0.WriteIoPort(_port + ADDRESS_REGISTER_OFFSET, register);
            Ring0.WriteIoPort(_port + DATA_REGISTER_OFFSET, value);
        }
        else
        {
            byte page = (byte)(address >> 8);
            byte index = (byte)(address & 0xFF);

            //wait for access, access == EC_SPACE_PAGE_SELECT
            //timeout: after 500ms, abort and force access
            byte access;

            DateTime timeout = DateTime.UtcNow.AddMilliseconds(500);
            while (true)
            {
                access = Ring0.ReadIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET);
                if (access == EC_SPACE_PAGE_SELECT || DateTime.UtcNow > timeout)
                    break;

                System.Threading.Thread.Sleep(1);
            }

            if (access != EC_SPACE_PAGE_SELECT)
            {
                // Failed to gain access: force register access
                Ring0.WriteIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET, EC_SPACE_PAGE_SELECT);
            }

            Ring0.WriteIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET, page);
            Ring0.WriteIoPort(_port + EC_SPACE_INDEX_REGISTER_OFFSET, index);
            Ring0.WriteIoPort(_port + EC_SPACE_DATA_REGISTER_OFFSET, value);

            //free access for other instances
            Ring0.WriteIoPort(_port + EC_SPACE_PAGE_REGISTER_OFFSET, EC_SPACE_PAGE_SELECT);
        }
    }

    private bool IsNuvotonVendor()
    {
        return Chip is Chip.NCT6683D or Chip.NCT6686D or Chip.NCT6687D || ((ReadByte(VENDOR_ID_HIGH_REGISTER) << 8) | ReadByte(VENDOR_ID_LOW_REGISTER)) == NUVOTON_VENDOR_ID;
    }

    private void SaveDefaultFanControl(int index)
    {
        if (!_restoreDefaultFanControlRequired[index])
        {
            if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
            {
                _initialFanControlMode[index] = ReadByte(FAN_CONTROL_MODE_REG[index]);
            }
            else
            {
                byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
                byte bitMask = (byte)(0x01 << index);
                _initialFanControlMode[index] = (byte)(mode & bitMask);
            }

            _initialFanPwmCommand[index] = ReadByte(FAN_PWM_COMMAND_REG[index]);
            _restoreDefaultFanControlRequired[index] = true;
        }
    }

    private void RestoreDefaultFanControl(int index)
    {
        if (_restoreDefaultFanControlRequired[index])
        {
            if (Chip is not Chip.NCT6683D and not Chip.NCT6686D and not Chip.NCT6687D)
            {
                WriteByte(FAN_CONTROL_MODE_REG[index], _initialFanControlMode[index]);
                WriteByte(FAN_PWM_COMMAND_REG[index], _initialFanPwmCommand[index]);
            }
            else
            {
                byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
                mode = (byte)(mode & ~_initialFanControlMode[index]);
                WriteByte(FAN_CONTROL_MODE_REG[index], mode);

                WriteByte(FAN_PWM_REQUEST_REG[index], 0x80);
                Thread.Sleep(50);

                WriteByte(FAN_PWM_COMMAND_REG[index], _initialFanPwmCommand[index]);
                WriteByte(FAN_PWM_REQUEST_REG[index], 0x40);
                Thread.Sleep(50);
            }

            _restoreDefaultFanControlRequired[index] = false;
        }
    }

    private void DisableIOSpaceLock()
    {
        if (Chip is not Chip.NCT6791D and
            not Chip.NCT6792D and
            not Chip.NCT6792DA and
            not Chip.NCT6793D and
            not Chip.NCT6795D and
            not Chip.NCT6796D and
            not Chip.NCT6796DR and
            not Chip.NCT6797D and
            not Chip.NCT6798D and
            not Chip.NCT6799D)
        {
            return;
        }

        // the lock is disabled already if the vendor ID can be read
        if (IsNuvotonVendor())
            return;

        _lpcPort.WinbondNuvotonFintekEnter();
        _lpcPort.NuvotonDisableIOSpaceLock();
        _lpcPort.WinbondNuvotonFintekExit();
    }

    [SuppressMessage("ReSharper", "InconsistentNaming")]
    private enum SourceNct6771F : byte
    {
        SYSTIN = 1,
        CPUTIN = 2,
        AUXTIN = 3,
        PECI_0 = 5
    }

    [SuppressMessage("ReSharper", "InconsistentNaming")]
    private enum SourceNct6776F : byte
    {
        SYSTIN = 1,
        CPUTIN = 2,
        AUXTIN = 3,
        PECI_0 = 12
    }

    [SuppressMessage("ReSharper", "InconsistentNaming")]
    private enum SourceNct67Xxd : byte
    {
        SYSTIN = 1,
        CPUTIN = 2,
        AUXTIN0 = 3,
        AUXTIN1 = 4,
        AUXTIN2 = 5,
        AUXTIN3 = 6,
        AUXTIN4 = 7,
        SMBUSMASTER0 = 8,
        SMBUSMASTER1 = 9,
        TSENSOR = 10,
        PECI_0 = 16,
        PECI_1 = 17,
        PCH_CHIP_CPU_MAX_TEMP = 18,
        PCH_CHIP_TEMP = 19,
        PCH_CPU_TEMP = 20,
        PCH_MCH_TEMP = 21,
        AGENT0_DIMM0 = 22,
        AGENT0_DIMM1 = 23,
        AGENT1_DIMM0 = 24,
        AGENT1_DIMM1 = 25,
        BYTE_TEMP0 = 26,
        BYTE_TEMP1 = 27,
        PECI_0_CAL = 28,
        PECI_1_CAL = 29,
        VIRTUAL_TEMP = 31
    }

    [SuppressMessage("ReSharper", "InconsistentNaming")]
    private enum SourceNct610X : byte
    {
        SYSTIN = 1,
        CPUTIN = 2,
        AUXTIN = 3,
        PECI_0 = 12
    }

    // ReSharper disable InconsistentNaming
    private const uint ADDRESS_REGISTER_OFFSET = 0x05;
    private const byte BANK_SELECT_REGISTER = 0x4E;
    private const uint DATA_REGISTER_OFFSET = 0x06;

    // NCT668X
    private const uint EC_SPACE_PAGE_REGISTER_OFFSET = 0x04;
    private const uint EC_SPACE_INDEX_REGISTER_OFFSET = 0x05;
    private const uint EC_SPACE_DATA_REGISTER_OFFSET = 0x06;
    private const byte EC_SPACE_PAGE_SELECT = 0xFF;

    private const ushort NUVOTON_VENDOR_ID = 0x5CA3;

    private readonly ushort[] FAN_CONTROL_MODE_REG;
    private readonly ushort[] FAN_PWM_COMMAND_REG;
    private readonly ushort[] FAN_PWM_OUT_REG;
    private readonly ushort[] FAN_PWM_REQUEST_REG;

    private readonly ushort VENDOR_ID_HIGH_REGISTER;
    private readonly ushort VENDOR_ID_LOW_REGISTER;

    // ReSharper restore InconsistentNaming
}
