#!/usr/bin/python3
# SPDX-License-Identifier: MIT

from datetime import timedelta
from amd_debug.common import print_color


class S0i3Failure:
    """Base class for all S0i3 failures"""

    def __init__(self):
        self.explanation = ""
        self.url = ""
        self.description = ""

    def get_failure(self):
        """Prints the failure message"""
        if self.description:
            print_color(self.description, "🚦")
        if self.explanation:
            print(self.explanation)
        if self.url:
            print(f"For more information on this failure see:{self.url}")

    def get_description(self) -> str:
        """Returns the description of the failure"""
        return self.description

    def __str__(self) -> str:
        if self.url:
            url = f"For more information on this failure see:{self.url}"
        else:
            url = ""
        return f"{self.explanation}{url}"


class RtcAlarmWrong(S0i3Failure):
    """RTC alarm is not configured to use ACPI"""

    def __init__(self):
        super().__init__()
        self.description = "rtc_cmos is not configured to use ACPI alarm"
        self.explanation = (
            "Some problems can occur during wakeup cycles if the HPET RTC "
            "emulation is used to wake systems. This can manifest in "
            "unexpected wakeups or high power consumption."
        )
        self.url = "https://github.com/systemd/systemd/issues/24279"


class MissingAmdgpu(S0i3Failure):
    """AMDGPU driver is missing"""

    def __init__(self):
        super().__init__()
        self.description = "AMDGPU driver is missing"
        self.explanation = (
            "The amdgpu driver is used for hardware acceleration as well "
            "as coordination of the power states for certain IP blocks on the SOC. "
            "Be sure that you have enabled CONFIG_AMDGPU in your kernel."
        )


class MissingAmdgpuFirmware(S0i3Failure):
    """AMDGPU firmware is missing"""

    def __init__(self, errors):
        super().__init__()
        self.description = "AMDGPU firmware is missing"
        self.explanation = (
            "The amdgpu driver loads firmware from /lib/firmware/amdgpu "
            "In some cases missing firmware will prevent a successful "
            "suspend cycle."
            "Upgrade to a newer snapshot at https://gitlab.com/kernel-firmware/linux-firmware"
        )
        self.url = "https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1053856"
        for error in errors:
            self.explanation += f"{error}"


class AmdgpuPpFeatureMask(S0i3Failure):
    """AMDGPU ppfeaturemask has been changed"""

    def __init__(self):
        super().__init__()
        self.description = "AMDGPU ppfeaturemask changed"
        self.explanation = (
            "The ppfeaturemask for the amdgpu driver has been changed "
            "Modifying this from the defaults may cause the system to not "
            "enter hardware sleep."
        )
        self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/2808#note_2379968"


class MissingAmdPmc(S0i3Failure):
    """AMD-PMC driver is missing"""

    def __init__(self):
        super().__init__()
        self.description = "AMD-PMC driver is missing"
        self.explanation = (
            "The amd-pmc driver is required for the kernel to instruct the "
            "soc to enter the hardware sleep state. "
            "Be sure that you have enabled CONFIG_AMD_PMC in your kernel. "
            ""
            "If CONFIG_AMD_PMC is enabled but the amd-pmc driver isn't loading "
            "then you may have found a bug and should report it."
        )


class MissingThunderbolt(S0i3Failure):
    """Thunderbolt driver is missing"""

    def __init__(self):
        super().__init__()
        self.description = "thunderbolt driver is missing"
        self.explanation = (
            "The thunderbolt driver is required for the USB4 routers included "
            "with the SOC to enter the proper power states. "
            "Be sure that you have enabled CONFIG_USB4 in your kernel."
        )


class MissingXhciHcd(S0i3Failure):
    """xhci_hcd driver is missing"""

    def __init__(self):
        super().__init__()
        self.description = "xhci_hcd driver is missing"
        self.explanation = (
            "The xhci_hcd driver is required for the USB3 controllers included "
            "with the SOC to enter the proper power states. "
            "Be sure that you have enabled CONFIG_XHCI_PCI in your kernel."
        )


class MissingDriver(S0i3Failure):
    """driver is missing"""

    def __init__(self, slot):
        super().__init__()
        self.description = f"{slot} driver is missing"
        self.explanation = (
            f"No driver has been bound to PCI device {slot} "
            "Without a driver, the hardware may be able to enter a low power "
            "state, but there may be spurious wake up events."
        )


class AcpiBiosError(S0i3Failure):
    """ACPI BIOS errors detected"""

    def __init__(self, errors):
        super().__init__()
        self.description = "ACPI BIOS Errors detected"
        self.explanation = (
            "When running a firmware component utilized for s2idle "
            "the ACPI interpreter in the Linux kernel encountered some "
            "problems. This usually means it's a bug in the system BIOS "
            "that should be fixed the system manufacturer."
            ""
            "You may have problems with certain devices after resume or high "
            "power consumption when this error occurs."
        )
        for error in errors:
            self.explanation += f"{error}"


class UnsupportedModel(S0i3Failure):
    """Unsupported CPU model"""

    def __init__(self):
        super().__init__()
        self.description = "Unsupported CPU model"
        self.explanation = (
            "This model does not support hardware s2idle. "
            "Attempting to run s2idle will use a pure software suspend "
            "and will not yield tangible power savings."
        )


class UserNvmeConfiguration(S0i3Failure):
    """User has disabled NVME ACPI support"""

    def __init__(self):
        super().__init__()
        self.description = "NVME ACPI support is disabled"
        self.explanation = (
            "The kernel command line has been configured to not support "
            "NVME ACPI support. This is required for the NVME device to "
            "enter the proper power state."
        )


class AcpiNvmeStorageD3Enable(S0i3Failure):
    """NVME device is missing ACPI attributes"""

    def __init__(self, disk, num_ssds):
        super().__init__()
        self.description = f"{disk} missing ACPI attributes"
        self.explanation = (
            "An NVME device was found, but it doesn't specify the StorageD3Enable "
            "attribute in the device specific data (_DSD). "
            "This is a BIOS bug, but it may be possible to work around in the kernel. "
        )
        if num_ssds > 1:
            self.explanation += (
                ""
                "If you added an aftermarket SSD to your system, the system vendor might not have added this "
                "property to the BIOS for the second port which could cause this behavior. "
                ""
                "Please re-run this script with the --acpidump argument and file a bug to "
                "investigate."
            )
        self.url = "https://bugzilla.kernel.org/show_bug.cgi?id=216440"


class DevSlpHostIssue(S0i3Failure):
    """AHCI controller doesn't support DevSlp"""

    def __init__(self):
        super().__init__()
        self.description = "AHCI controller doesn't support DevSlp"
        self.explanation = (
            "The AHCI controller is not configured to support DevSlp. "
            "This must be enabled in BIOS for s2idle in Linux."
        )


class DevSlpDiskIssue(S0i3Failure):
    """SATA disk doesn't support DevSlp"""

    def __init__(self):
        super().__init__()
        self.description = "SATA disk doesn't support DevSlp"
        self.explanation = (
            "The SATA disk does not support DevSlp. "
            "s2idle in Linux requires SATA disks that support this feature."
        )


class SleepModeWrong(S0i3Failure):
    """System is not configured for Modern Standby"""

    def __init__(self):
        super().__init__()
        self.description = (
            "The system hasn't been configured for Modern Standby in BIOS setup"
        )
        self.explanation = (
            "AMD systems must be configured for Modern Standby in BIOS setup "
            "for s2idle to function properly in Linux. "
            "On some OEM systems this is referred to as 'Windows' sleep mode. "
            "If the BIOS is configured for S3 and you manually select s2idle "
            "in /sys/power/mem_sleep, the system will not enter the deepest hardware state."
        )


class DeepSleep(S0i3Failure):
    """Deep sleep is configured on the kernel command line"""

    def __init__(self):
        super().__init__()
        self.description = (
            "The kernel command line is asserting the system to use deep sleep"
        )
        self.explanation = (
            "Adding mem_sleep_default=deep doesn't work on AMD systems. "
            "Please remove it from the kernel command line."
        )


class FadtWrong(S0i3Failure):
    """FADT doesn't support low power idle"""

    def __init__(self):
        super().__init__()
        self.description = (
            "The kernel didn't emit a message that low power idle was supported"
        )
        self.explanation = (
            "Low power idle is a bit documented in the FADT to indicate that "
            "low power idle is supported. "
            "Only newer kernels support emitting this message, so if you run on "
            "an older kernel you may get a false negative. "
            "When launched as root this script will try to directly introspect the "
            "ACPI tables to confirm this."
        )


class Irq1Workaround(S0i3Failure):
    """IRQ1 wakeup source is active"""

    def __init__(self):
        super().__init__()
        self.description = "The wakeup showed an IRQ1 wakeup source, which might be a platform firmware bug"
        self.explanation = (
            "A number of Renoir, Lucienne, Cezanne, & Barcelo platforms have a platform firmware "
            "bug where IRQ1 is triggered during s0i3 resume. "
            "You may have tripped up on this bug as IRQ1 was active during resume. "
            "If you didn't press a keyboard key to wakeup the system then this can be "
            "the cause of spurious wakeups."
            ""
            "To fix it, first try to upgrade to the latest firmware from your manufacturer. "
            "If you're already upgraded to the latest firmware you can use one of two workarounds: "
            " 1. Manually disable wakeups from IRQ1 by running this command each boot: "
            " echo 'disabled' | sudo tee /sys/bus/serio/devices/serio0/power/wakeup "
            " 2. Use the below linked patch in your kernel."
        )
        self.url = "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/drivers/platform/x86/amd/pmc.c?id=8e60615e8932167057b363c11a7835da7f007106"


class KernelRingBufferWrapped(S0i3Failure):
    """Kernel ringbuffer has wrapped"""

    def __init__(self):
        super().__init__()
        self.description = "Kernel ringbuffer has wrapped"
        self.explanation = (
            "This script relies upon analyzing the kernel log for markers. "
            "The kernel's log provided by dmesg uses a ring buffer. "
            "When the ring buffer fills up it will wrap around and overwrite old messages. "
            ""
            "In this case it's not possible to look for some of these markers "
            ""
            "Passing the pre-requisites check won't be possible without rebooting the machine. "
            "If you are sure your system meets pre-requisites, you can re-run the script using. "
            "the systemd logger or with --force."
        )


class AmdHsmpBug(S0i3Failure):
    """AMD HSMP is built into the kernel"""

    def __init__(self):
        super().__init__()
        self.description = "amd-hsmp built in to kernel"
        self.explanation = (
            "The kernel has been compiled with CONFIG_AMD_HSMP=y. "
            "This has been shown to cause suspend failures on some systems. "
            ""
            "Either recompile the kernel without CONFIG_AMD_HSMP, "
            "or use initcall_blacklist=hsmp_plt_init on your kernel command line to avoid triggering problems "
            ""
        )
        self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/2414"


class WCN6855Bug(S0i3Failure):
    """WCN6855 firmware causes spurious wakeups"""

    def __init__(self):
        super().__init__()
        self.description = "The firmware loaded for the WCN6855 causes spurious wakeups"
        self.explanation = (
            "During s2idle on AMD systems PCIe devices are put into D3cold. During wakeup they're transitioned back "
            "into the state they were before s2idle.  For many implementations this is D3hot. "
            "If an ACPI event has been triggered by the EC, the hardware will resume from s2idle, "
            "but the kernel should process the event and then put it back into s2idle. "
            ""
            "When this bug occurs, a GPIO connected to the WLAN card is active on the system making "
            "he GPIO controller IRQ also active.  The kernel sees that the ACPI event IRQ and GPIO "
            "controller IRQ are both active and resumes the system. "
            ""
            "Some non-exhaustive events that will trigger this behavior: "
            " * Suspending the system and then closing the lid. "
            " * Suspending the system and then unplugging the AC adapter. "
            " * Suspending the system and the EC notifying the OS of a battery level change. "
            ""
            "This issue is fixed by updated WCN6855 firmware which will avoid triggering the GPIO. "
            "The version string containing the fix is 'WLAN.HSP.1.1-03125-QCAHSPSWPL_V1_V2_SILICONZ_LITE-3.6510.23' "
        )
        self.url = "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/commit/?id=c7a57ef688f7d99d8338a5d8edddc8836ff0e6de"


class I2CHidBug(S0i3Failure):
    """I2C HID device causes spurious wakeups"""

    def __init__(self, name, remediation):
        super().__init__()
        self.description = f"The {name} device has been reported to cause high power consumption and spurious wakeups"
        self.explanation = (
            "I2C devices work in an initiator/receiver relationship where the device is the receiver. In order for the receiver to indicate "
            "the initiator needs to read data they will assert an attention GPIO pin. "
            "When a device misbehaves it may assert this pin spuriously which can cause the SoC to wakeup prematurely. "
            "This typically manifests as high power consumption at runtime and spurious wakeups at suspend. "
            ""
            "This issue can be worked around by unbinding the device from the kernel using this command: "
            ""
            f"{remediation}"
            ""
            "To fix this issue permanently the kernel will need to avoid binding to this device. "
        )
        self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/2812"


class SpuriousWakeup(S0i3Failure):
    """System woke up prematurely"""

    def __init__(self, requested, wake):
        super().__init__()
        self.description = (
            f"Userspace wasn't asleep at least {timedelta(seconds=requested)}"
        )
        self.explanation = (
            f"The system was programmed to sleep for {timedelta(seconds=requested)}, but woke up prematurely after {wake}. "
            "This typically happens when the system was woken up from a non-timer based source. "
            "If you didn't intentionally wake it up, then there may be a kernel or firmware bug."
        )


class LowHardwareSleepResidency(S0i3Failure):
    """System had low hardware sleep residency"""

    def __init__(self, duration, percent):
        super().__init__()
        self.description = "System had low hardware sleep residency"
        self.explanation = (
            f"The system was asleep for {timedelta(seconds=duration)}, but only spent {percent/100:.2%} "
            "of this time in a hardware sleep state.  In sleep cycles that are at least "
            "60 seconds long it's expected you spend above 90 percent of the cycle in "
            "hardware sleep."
        )


class MSRFailure(S0i3Failure):
    """MSR access failed"""

    def __init__(self):
        super().__init__()
        self.description = "PC6 or CC6 state disabled"
        self.explanation = (
            "The PC6 state of the package or the CC6 state of CPU cores was disabled. "
            "This will prevent the system from getting to the deepest sleep state over suspend."
        )


class TaintedKernel(S0i3Failure):
    """Kernel is tainted"""

    def __init__(self):
        super().__init__()
        self.description = "Kernel is tainted"
        self.explanation = (
            "A tainted kernel may exhibit unpredictable bugs that are difficult for this script to characterize. "
            "If this is intended behavior run the tool with --force. "
        )
        self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/3089"


class DMArNotEnabled(S0i3Failure):
    """DMAr is not enabled"""

    def __init__(self):
        super().__init__()
        self.description = "Pre-boot DMA protection disabled"
        self.explanation = (
            "Pre-boot IOMMU DMA protection has been disabled. "
            "When the IOMMU is enabled this platform requires pre-boot DMA protection for suspend to work. "
        )


class MissingIommuACPI(S0i3Failure):
    """IOMMU ACPI table errors"""

    def __init__(self, device):
        super().__init__()
        self.description = f"Device {device} missing from ACPI tables"
        self.explanation = (
            "The ACPI device {device} is required for suspend to work when the IOMMU is enabled. "
            "Please check your BIOS settings and if configured correctly, report a bug to your system vendor."
        )
        self.url = "https://gitlab.freedesktop.org/drm/amd/-/issues/3738#note_2667140"


class MissingIommuPolicy(S0i3Failure):
    """ACPI table errors"""

    def __init__(self, device):
        super().__init__()
        self.description = f"Device {device} does not have IOMMU policy applied"
        self.explanation = (
            f"The ACPI device {device} is present but no IOMMU policy was set for it."
            "This generally happens if the HID or UID don't match the ACPI IVRS table."
        )


class IommuPageFault(S0i3Failure):
    """IOMMU Page fault"""

    def __init__(self, device):
        super().__init__()
        self.description = f"Page fault reported for {device}"
        self.explanation = (
            f"The IOMMU reports a page fault caused by {device}. This can prevent suspend/resume from functioning properly"
            "The page fault can be the device itself, a problem in the firmware or a problem in the kernel."
            "Report a bug for further triage and investigation."
        )


class SMTNotEnabled(S0i3Failure):
    """SMT is not enabled"""

    def __init__(self):
        super().__init__()
        self.description = "SMT is not enabled"
        self.explanation = (
            "Disabling SMT prevents cores from going into the correct state."
        )


class ASpmWrong(S0i3Failure):
    """ASPM is overridden"""

    def __init__(self):
        super().__init__()
        self.description = "ASPM is overridden"
        self.explanation = (
            " Modifying ASPM may prevent PCIe devices from going into the "
            " correct state and lead to system stability issues. "
        )


class UnservicedGpio(S0i3Failure):
    """GPIO is not serviced"""

    def __init__(self):
        super().__init__()
        self.description = "GPIO interrupt is not serviced"
        self.explanation = (
            "All GPIO controllers interrupts must be serviced to enter "
            "hardware sleep."
            "Make sure that all drivers necessary to service GPIOs are loaded. "
            "The most common cause is that i2c-hid-acpi is not loaded but the "
            "machine contains an I2C touchpad."
        )


class DmiNotSetup(S0i3Failure):
    """DMI isn't setup"""

    def __init__(self):
        super().__init__()
        self.description = "DMI data was not scanned"
        self.explanation = (
            "If DMI data hasn't been scanned then quirks that are dependent "
            "upon DMI won't be loaded. "
            "Most notably, this will prevent the rtc-cmos driver from setting. "
            "up properly by default. It may also prevent other drivers from working."
        )


class LimitedCores(S0i3Failure):
    """Number of CPU cores limited"""

    def __init__(self, actual_cores, max_cores):
        super().__init__()
        self.description = "CPU cores have been limited"
        self.explanation = (
            f"The CPU cores have been limited to {max_cores}, but the system "
            f"actually has {actual_cores}. Limiting the cores will prevent the "
            "the system from going into a hardware sleep state. "
            "This is typically solved by increasing the kernel config CONFIG_NR_CPUS."
        )


class RogAllyOldMcu(S0i3Failure):
    """MCU firwmare is too old"""

    def __init__(self, vmin, actual):
        super().__init__()
        self.description = "Rog Ally MCU firmware is too old"
        self.explanation = (
            f"The MCU is version {actual}, but needs to be at least {vmin}"
            f"to avoid major issues with interactions with suspend"
        )


class RogAllyMcuPowerSave(S0i3Failure):
    """MCU powersave is disabled"""

    def __init__(self):
        super().__init__()
        self.description = "Rog Ally MCU power save is disabled"
        self.explanation = (
            "The MCU powersave feature is disabled which will cause problems "
            "with the controller after suspend/resume."
        )


class DmcubTooOld(S0i3Failure):
    """DMCUB microcode is too old"""

    def __init__(self, current, expected):
        super().__init__()
        self.description = "DMCUB microcode is too old"
        self.explanation = (
            f"The DMCUB microcode version {hex(current)} is older than the"
            f"minimum suggested version {hex(expected)}."
        )
