// 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.Runtime.InteropServices;
using System.Threading;
using LibreHardwareMonitor.Hardware.Cpu;

namespace LibreHardwareMonitor.Hardware.Motherboard.Lpc;

/// <summary>
/// This is a controller present on some Gigabyte motherboards for both Intel and AMD, that is in custom firmware
/// loaded onto the 2nd ITE EC.
/// It can be accessed by using memory mapped IO, mapping its internal RAM onto main RAM via the ISA Bridge.
/// This class can disable it so that the regular IT87XX code can drive the fans.
/// </summary>
internal class IsaBridgeGigabyteController : IGigabyteController
{
    private const uint ControllerAddressRange = 0xFF;
    private const int ControllerEnableRegister = 0x47;
    private const uint ControllerFanControlArea = 0x900;

    /// <summary>
    /// Base address in PCI RAM that maps to the EC's RAM
    /// </summary>
    private readonly uint _controllerBaseAddress;

    private readonly Vendor _vendor;

    private bool? _initialState;

    public IsaBridgeGigabyteController(uint address, Vendor vendor)
    {
        _controllerBaseAddress = address;
        _vendor = vendor;
    }

    /// <summary>
    /// Enable/Disable Fan Control
    /// </summary>
    /// <param name="enabled"></param>
    /// <returns>true on success</returns>
    public bool Enable(bool enabled)
    {
        return _vendor switch
        {
            Vendor.Intel => IntelEnable(enabled),
            Vendor.AMD => AmdEnable(enabled),
            _ => false
        };
    }

    private bool IntelEnable(bool enabled)
    {
        if (!Mutexes.WaitPciBus(10))
            return false;

        bool result = false;

        uint intelIsaBridgeAddress = Ring0.GetPciAddress(0x0, 0x1F, 0x0);

        const uint ioOrMemoryPortDecodeEnableRegister = 0xD8;
        const uint romAddressRange2Register = 0x98;

        uint controllerFanControlAddress = _controllerBaseAddress + ControllerFanControlArea;

        Ring0.ReadPciConfig(intelIsaBridgeAddress, ioOrMemoryPortDecodeEnableRegister, out uint originalDecodeEnableRegister);
        Ring0.ReadPciConfig(intelIsaBridgeAddress, romAddressRange2Register, out uint originalRomAddressRegister);

        bool originalMmIoEnabled = false;
        if (!enabled)
        {
            originalMmIoEnabled = ((int)originalDecodeEnableRegister & 1) == 0 || ((int)originalRomAddressRegister & 1) == 1;
        }
        else
        {
            originalMmIoEnabled = ((int)originalDecodeEnableRegister & 1) == 0 && ((int)originalRomAddressRegister & 1) == 1;
        }

        if (enabled == originalMmIoEnabled)
        {
            result = Enable(enabled, new IntPtr(controllerFanControlAddress));
            Mutexes.ReleasePciBus();
            return result;
        }

        uint lpcBiosDecodeEnable;
        uint lpcMemoryRange;
        if (enabled)
        {
            lpcBiosDecodeEnable = ioOrMemoryPortDecodeEnableRegister & ~(uint)(1 << 0);
            lpcMemoryRange = romAddressRange2Register | (uint)(1 << 0);
        }
        else
        {
            lpcBiosDecodeEnable = Convert.ToUInt32(ioOrMemoryPortDecodeEnableRegister | (uint)(1 << 0));
            lpcMemoryRange = Convert.ToUInt32(romAddressRange2Register & ~(uint)(1 << 0));
        }

        Ring0.WritePciConfig(intelIsaBridgeAddress, ioOrMemoryPortDecodeEnableRegister, lpcBiosDecodeEnable);
        Ring0.WritePciConfig(intelIsaBridgeAddress, romAddressRange2Register, lpcMemoryRange);

        result = Enable(enabled, new IntPtr(controllerFanControlAddress));

        Mutexes.ReleasePciBus();

        return result;
    }

    private bool AmdEnable(bool enabled)
    {
        if (!Mutexes.WaitPciBus(10))
            return false;

        // see D14F3x https://www.amd.com/system/files/TechDocs/55072_AMD_Family_15h_Models_70h-7Fh_BKDG.pdf
        uint amdIsaBridgeAddress = Ring0.GetPciAddress(0x0, 0x14, 0x3);

        const uint ioOrMemoryPortDecodeEnableRegister = 0x48;
        const uint memoryRangePortEnableMask = 0x1 << 5;
        const uint pciMemoryAddressForLpcTargetCyclesRegister = 0x60;
        const uint romAddressRange2Register = 0x6C;

        uint controllerFanControlAddress = _controllerBaseAddress + ControllerFanControlArea;

        uint pciAddressStart = _controllerBaseAddress >> 0x10;
        uint pciAddressEnd = pciAddressStart + 1;

        uint enabledPciMemoryAddressRegister = pciAddressEnd << 0x10 | pciAddressStart;
        uint enabledRomAddressRegister = 0xFFFFU << 0x10 | pciAddressEnd;

        Ring0.ReadPciConfig(amdIsaBridgeAddress, ioOrMemoryPortDecodeEnableRegister, out uint originalDecodeEnableRegister);
        Ring0.ReadPciConfig(amdIsaBridgeAddress, pciMemoryAddressForLpcTargetCyclesRegister, out uint originalPciMemoryAddressRegister);
        Ring0.ReadPciConfig(amdIsaBridgeAddress, romAddressRange2Register, out uint originalRomAddressRegister);

        bool originalMmIoEnabled = (originalDecodeEnableRegister & memoryRangePortEnableMask) != 0 &&
                                   originalPciMemoryAddressRegister == enabledPciMemoryAddressRegister &&
                                   originalRomAddressRegister == enabledRomAddressRegister;

        if (!originalMmIoEnabled)
        {
            Ring0.WritePciConfig(amdIsaBridgeAddress, ioOrMemoryPortDecodeEnableRegister, originalDecodeEnableRegister | memoryRangePortEnableMask);
            Ring0.WritePciConfig(amdIsaBridgeAddress, pciMemoryAddressForLpcTargetCyclesRegister, enabledPciMemoryAddressRegister);
            Ring0.WritePciConfig(amdIsaBridgeAddress, romAddressRange2Register, enabledRomAddressRegister);
        }

        bool result = Enable(enabled, new IntPtr(controllerFanControlAddress));

        // Restore previous values
        if (!originalMmIoEnabled)
        {
            Ring0.WritePciConfig(amdIsaBridgeAddress, ioOrMemoryPortDecodeEnableRegister, originalDecodeEnableRegister);
            Ring0.WritePciConfig(amdIsaBridgeAddress, pciMemoryAddressForLpcTargetCyclesRegister, originalPciMemoryAddressRegister);
            Ring0.WritePciConfig(amdIsaBridgeAddress, romAddressRange2Register, originalRomAddressRegister);
        }

        Mutexes.ReleasePciBus();

        return result;
    }

    private bool Enable(bool enabled, IntPtr pciMmIoBaseAddress)
    {
        // Map PCI memory to this process memory
        if (!InpOut.Open())
            return false;

        IntPtr mapped = InpOut.MapMemory(pciMmIoBaseAddress, ControllerAddressRange, out IntPtr handle);

        if (mapped == IntPtr.Zero)
            return false;

        bool current = Convert.ToBoolean(Marshal.ReadByte(mapped, ControllerEnableRegister));

        _initialState ??= current;

        // Update Controller State
        if (current != enabled)
        {
            Marshal.WriteByte(mapped, ControllerEnableRegister, Convert.ToByte(enabled));
            // Give it some time to see the change
            Thread.Sleep(500);
        }

        InpOut.UnmapMemory(handle, mapped);
        return true;
    }

    /// <summary>
    /// Restore settings back to initial values
    /// </summary>
    public void Restore()
    {
        if (_initialState.HasValue)
            Enable(_initialState.Value);
    }
}
