# Copyright (C) 2022 Rafael Leira, Naudit HPCN S.L.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2, as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA  02110-1301, USA.
#
################################################################
"""
This module contains the definition of the `Nvme` device interface type.
"""

from enum import Enum
import re
import humanfriendly
from typing import Optional, Iterator, Union, List
from ..common import CommonIface


class NvmeStatus(Enum):
    # Codes have been extraced from https://github.com/RobinTMiller/dt/blob/master/nvme_lib.h
    # Copyright (c) 2011-2014, Intel Corporation.

    # Generic Command
    NVME_SC_SUCCESS = 0X0
    NVME_SC_INVALID_OPCODE = 0X1
    NVME_SC_INVALID_FIELD = 0X2
    NVME_SC_CMDID_CONFLICT = 0X3
    NVME_SC_DATA_XFER_ERROR = 0X4
    NVME_SC_POWER_LOSS = 0X5
    NVME_SC_INTERNAL = 0X6
    NVME_SC_ABORT_REQ = 0X7
    NVME_SC_ABORT_QUEUE = 0X8
    NVME_SC_FUSED_FAIL = 0X9
    NVME_SC_FUSED_MISSING = 0XA
    NVME_SC_INVALID_NS = 0XB
    NVME_SC_CMD_SEQ_ERROR = 0XC
    NVME_SC_SGL_INVALID_LAST = 0XD
    NVME_SC_SGL_INVALID_COUNT = 0XE
    NVME_SC_SGL_INVALID_DATA = 0XF
    NVME_SC_SGL_INVALID_METADATA = 0X10
    NVME_SC_SGL_INVALID_TYPE = 0X11
    NVME_SC_CMB_INVALID_USE = 0X12
    NVME_SC_PRP_INVALID_OFFSET = 0X13
    NVME_SC_ATOMIC_WRITE_UNIT_EXCEEDED = 0X14
    NVME_SC_OPERATION_DENIED = 0X15
    NVME_SC_SGL_INVALID_OFFSET = 0X16

    NVME_SC_INCONSISTENT_HOST_ID = 0X18
    NVME_SC_KEEP_ALIVE_EXPIRED = 0X19
    NVME_SC_KEEP_ALIVE_INVALID = 0X1A
    NVME_SC_PREEMPT_ABORT = 0X1B
    NVME_SC_SANITIZE_FAILED = 0X1C
    NVME_SC_SANITIZE_IN_PROGRESS = 0X1D

    NVME_SC_NS_WRITE_PROTECTED = 0X20
    NVME_SC_CMD_INTERRUPTED = 0X21
    NVME_SC_TRANSIENT_TRANSPORT = 0X22

    NVME_SC_LBA_RANGE = 0X80
    NVME_SC_CAP_EXCEEDED = 0X81
    NVME_SC_NS_NOT_READY = 0X82
    NVME_SC_RESERVATION_CONFLICT = 0X83
    NVME_SC_FORMAT_IN_PROGRESS = 0X84

    # Command Specific Status:
    NVME_SC_CQ_INVALID = 0X100
    NVME_SC_QID_INVALID = 0X101
    NVME_SC_QUEUE_SIZE = 0X102
    NVME_SC_ABORT_LIMIT = 0X103
    NVME_SC_ABORT_MISSING = 0X104
    NVME_SC_ASYNC_LIMIT = 0X105
    NVME_SC_FIRMWARE_SLOT = 0X106
    NVME_SC_FIRMWARE_IMAGE = 0X107
    NVME_SC_INVALID_VECTOR = 0X108
    NVME_SC_INVALID_LOG_PAGE = 0X109
    NVME_SC_INVALID_FORMAT = 0X10A
    NVME_SC_FW_NEEDS_CONV_RESET = 0X10B
    NVME_SC_INVALID_QUEUE = 0X10C
    NVME_SC_FEATURE_NOT_SAVEABLE = 0X10D
    NVME_SC_FEATURE_NOT_CHANGEABLE = 0X10E
    NVME_SC_FEATURE_NOT_PER_NS = 0X10F
    NVME_SC_FW_NEEDS_SUBSYS_RESET = 0X110
    NVME_SC_FW_NEEDS_RESET = 0X111
    NVME_SC_FW_NEEDS_MAX_TIME = 0X112
    NVME_SC_FW_ACTIVATE_PROHIBITED = 0X113
    NVME_SC_OVERLAPPING_RANGE = 0X114
    NVME_SC_NS_INSUFFICIENT_CAP = 0X115
    NVME_SC_NS_ID_UNAVAILABLE = 0X116
    NVME_SC_NS_ALREADY_ATTACHED = 0X118
    NVME_SC_NS_IS_PRIVATE = 0X119
    NVME_SC_NS_NOT_ATTACHED = 0X11A
    NVME_SC_THIN_PROV_NOT_SUPP = 0X11B
    NVME_SC_CTRL_LIST_INVALID = 0X11C
    NVME_SC_DEVICE_SELF_TEST_IN_PROGRESS = 0X11D
    NVME_SC_BP_WRITE_PROHIBITED = 0X11E
    NVME_SC_INVALID_CTRL_ID = 0X11F
    NVME_SC_INVALID_SECONDARY_CTRL_STATE = 0X120
    NVME_SC_INVALID_NUM_CTRL_RESOURCE = 0X121
    NVME_SC_INVALID_RESOURCE_ID = 0X122
    NVME_SC_PMR_SAN_PROHIBITED = 0X123
    NVME_SC_ANA_INVALID_GROUP_ID = 0X124
    NVME_SC_ANA_ATTACH_FAIL = 0X125

    # Command Set Specific - Namespace Types commands:
    NVME_SC_IOCS_NOT_SUPPORTED = 0X129
    NVME_SC_IOCS_NOT_ENABLED = 0X12A
    NVME_SC_IOCS_COMBINATION_REJECTED = 0X12B
    NVME_SC_INVALID_IOCS = 0X12C

    # I/O Command Set Specific - NVM commands:
    NVME_SC_BAD_ATTRIBUTES = 0X180
    NVME_SC_INVALID_PI = 0X181
    NVME_SC_READ_ONLY = 0X182
    NVME_SC_CMD_SIZE_LIMIT_EXCEEDED = 0X183

    # I/O Command Set Specific - Fabrics commands:
    NVME_SC_CONNECT_FORMAT = 0X180
    NVME_SC_CONNECT_CTRL_BUSY = 0X181
    NVME_SC_CONNECT_INVALID_PARAM = 0X182
    NVME_SC_CONNECT_RESTART_DISC = 0X183
    NVME_SC_CONNECT_INVALID_HOST = 0X184

    NVME_SC_DISCOVERY_RESTART = 0X190
    NVME_SC_AUTH_REQUIRED = 0X191

    # I/O Command Set Specific - Zoned Namespace commands:
    NVME_SC_ZONE_BOUNDARY_ERROR = 0X1B8
    NVME_SC_ZONE_IS_FULL = 0X1B9
    NVME_SC_ZONE_IS_READ_ONLY = 0X1BA
    NVME_SC_ZONE_IS_OFFLINE = 0X1BB
    NVME_SC_ZONE_INVALID_WRITE = 0X1BC
    NVME_SC_TOO_MANY_ACTIVE_ZONES = 0X1BD
    NVME_SC_TOO_MANY_OPEN_ZONES = 0X1BE
    NVME_SC_ZONE_INVALID_STATE_TRANSITION = 0X1BF

    # Media and Data Integrity Errors:
    NVME_SC_WRITE_FAULT = 0X280
    NVME_SC_READ_ERROR = 0X281
    NVME_SC_GUARD_CHECK = 0X282
    NVME_SC_APPTAG_CHECK = 0X283
    NVME_SC_REFTAG_CHECK = 0X284
    NVME_SC_COMPARE_FAILED = 0X285
    NVME_SC_ACCESS_DENIED = 0X286
    NVME_SC_UNWRITTEN_BLOCK = 0X287

    # Path-related Errors:
    NVME_SC_INTERNAL_PATH_ERROR = 0X300
    NVME_SC_ANA_PERSISTENT_LOSS = 0X301
    NVME_SC_ANA_INACCESSIBLE = 0X302
    NVME_SC_ANA_TRANSITION = 0X303

    # Controller Detected Path errors
    NVME_SC_CTRL_PATHING_ERROR = 0X360

    # Host Detected Path Errors
    NVME_SC_HOST_PATHING_ERROR = 0X370
    NVME_SC_HOST_CMD_ABORT = 0X371

    NVME_SC_CRD = 0X1800
    NVME_SC_DNR = 0X4000


class NvmeError(object):
    """This class stores the nvme detected errors
    The expected error table looks like:

    Num   ErrCount  SQId   CmdId  Status  PELoc          LBA  NSID    VS
    0       1356     0  0x0012  0xc005  0x028            -     0     -

    Attributes:
        id                      : The error number or id
        count                   : The number of errors detected
        sqid                    : The submission queue id
        cmdid                   : The command id
        status                  : The status of the error
        peloc                   : The physical error location
        lba                     : The logical block address
        nsid                    : The namespace id
        vs                      : The vendor specific
        cs                      : The command specific
    """

    def __init__(self, num: int, errCount: int, sqId: int, cmdId: int, status: int, peLoc: int, lba: Optional[int] = None, nsid: Optional[int] = None, vs: Optional[int] = None):
        self.num: int = num
        self.errCount: int = errCount
        self.sqId: int = sqId
        self.cmdId: int = cmdId
        self.status: int = status
        self.peLoc: int = peLoc
        self.lba: Optional[int] = lba
        self.nsid: Optional[int] = nsid
        self.vs: Optional[int] = vs
        self.cs: Optional[int] = None

    @property
    def status_str(self) -> str:
        # try to convert the status to a string

        # Strings have been extraced from https://github.com/RobinTMiller/dt/blob/master/nvme_lib.c
        # Copyright (c) 2011-2014, Intel Corporation.

        status = self.status & 0x7ff

        if status == NvmeStatus.NVME_SC_SUCCESS:
            status_str = "SUCCESS: The command completed successfully"
        elif status == NvmeStatus.NVME_SC_INVALID_OPCODE:
            status_str = "INVALID_OPCODE: The associated command opcode field is not valid"
        elif status == NvmeStatus.NVME_SC_INVALID_FIELD:
            status_str = "INVALID_FIELD: A reserved coded value or an unsupported value in a defined field"
        elif status == NvmeStatus.NVME_SC_CMDID_CONFLICT:
            status_str = "CMDID_CONFLICT: The command identifier is already in use"
        elif status == NvmeStatus.NVME_SC_DATA_XFER_ERROR:
            status_str = "DATA_XFER_ERROR: Error while trying to transfer the data or metadata"
        elif status == NvmeStatus.NVME_SC_POWER_LOSS:
            status_str = "POWER_LOSS: Command aborted due to power loss notification"
        elif status == NvmeStatus.NVME_SC_INTERNAL:
            status_str = "INTERNAL: The command was not completed successfully due to an internal error"
        elif status == NvmeStatus.NVME_SC_ABORT_REQ:
            status_str = "ABORT_REQ: The command was aborted due to a Command Abort request"
        elif status == NvmeStatus.NVME_SC_ABORT_QUEUE:
            status_str = "ABORT_QUEUE: The command was aborted due to a Delete I/O Submission Queue request"
        elif status == NvmeStatus.NVME_SC_FUSED_FAIL:
            status_str = "FUSED_FAIL: The command was aborted due to the other command in a fused operation failing"
        elif status == NvmeStatus.NVME_SC_FUSED_MISSING:
            status_str = "FUSED_MISSING: The command was aborted due to a Missing Fused Command"
        elif status == NvmeStatus.NVME_SC_INVALID_NS:
            status_str = "INVALID_NS: The namespace or the format of that namespace is invalid"
        elif status == NvmeStatus.NVME_SC_CMD_SEQ_ERROR:
            status_str = "CMD_SEQ_ERROR: The command was aborted due to a protocol violation in a multicommand sequence"
        elif status == NvmeStatus.NVME_SC_SGL_INVALID_LAST:
            status_str = "SGL_INVALID_LAST: The command includes an invalid SGL Last Segment or SGL Segment descriptor."
        elif status == NvmeStatus.NVME_SC_SGL_INVALID_COUNT:
            status_str = "SGL_INVALID_COUNT: There is an SGL Last Segment descriptor or an SGL Segment descriptor in a location other than the last descriptor of a segment based on the length indicated."
        elif status == NvmeStatus.NVME_SC_SGL_INVALID_DATA:
            status_str = "SGL_INVALID_DATA: This may occur if the length of a Data SGL is too short."
        elif status == NvmeStatus.NVME_SC_SGL_INVALID_METADATA:
            status_str = "SGL_INVALID_METADATA: This may occur if the length of a Metadata SGL is too short"
        elif status == NvmeStatus.NVME_SC_SGL_INVALID_TYPE:
            status_str = "SGL_INVALID_TYPE: The type of an SGL Descriptor is a type that is not supported by the controller."
        elif status == NvmeStatus.NVME_SC_CMB_INVALID_USE:
            status_str = "CMB_INVALID_USE: The attempted use of the Controller Memory Buffer is not supported by the controller."
        elif status == NvmeStatus.NVME_SC_PRP_INVALID_OFFSET:
            status_str = "PRP_INVALID_OFFSET: The Offset field for a PRP entry is invalid."
        elif status == NvmeStatus.NVME_SC_ATOMIC_WRITE_UNIT_EXCEEDED:
            status_str = "ATOMIC_WRITE_UNIT_EXCEEDED: The length specified exceeds the atomic write unit size."
        elif status == NvmeStatus.NVME_SC_OPERATION_DENIED:
            status_str = "OPERATION_DENIED: The command was denied due to lack of access rights."
        elif status == NvmeStatus.NVME_SC_SGL_INVALID_OFFSET:
            status_str = "SGL_INVALID_OFFSET: The offset specified in a descriptor is invalid."
        elif status == NvmeStatus.NVME_SC_INCONSISTENT_HOST_ID:
            status_str = "INCONSISTENT_HOST_ID: The NVM subsystem detected the simultaneous use of 64-bit and 128-bit Host Identifier values on different controllers."
        elif status == NvmeStatus.NVME_SC_KEEP_ALIVE_EXPIRED:
            status_str = "KEEP_ALIVE_EXPIRED: The Keep Alive Timer expired."
        elif status == NvmeStatus.NVME_SC_KEEP_ALIVE_INVALID:
            status_str = "KEEP_ALIVE_INVALID: The Keep Alive Timeout value specified is invalid."
        elif status == NvmeStatus.NVME_SC_PREEMPT_ABORT:
            status_str = "PREEMPT_ABORT: The command was aborted due to a Reservation Acquire command with the Reservation Acquire Action (RACQA) set to 010b (Preempt and Abort)."
        elif status == NvmeStatus.NVME_SC_SANITIZE_FAILED:
            status_str = "SANITIZE_FAILED: The most recent sanitize operation failed and no recovery actions has been successfully completed"
        elif status == NvmeStatus.NVME_SC_SANITIZE_IN_PROGRESS:
            status_str = "SANITIZE_IN_PROGRESS: The requested function is prohibited while a sanitize operation is in progress"
        elif status == NvmeStatus.NVME_SC_IOCS_NOT_SUPPORTED:
            status_str = "IOCS_NOT_SUPPORTED: The I/O command set is not supported"
        elif status == NvmeStatus.NVME_SC_IOCS_NOT_ENABLED:
            status_str = "IOCS_NOT_ENABLED: The I/O command set is not enabled"
        elif status == NvmeStatus.NVME_SC_IOCS_COMBINATION_REJECTED:
            status_str = "IOCS_COMBINATION_REJECTED: The I/O command set combination is rejected"
        elif status == NvmeStatus.NVME_SC_INVALID_IOCS:
            status_str = "INVALID_IOCS: the I/O command set is invalid"
        elif status == NvmeStatus.NVME_SC_LBA_RANGE:
            status_str = "LBA_RANGE: The command references a LBA that exceeds the size of the namespace"
        elif status == NvmeStatus.NVME_SC_NS_WRITE_PROTECTED:
            status_str = "NS_WRITE_PROTECTED: The command is prohibited while the namespace is write protected by the host."
        elif status == NvmeStatus.NVME_SC_TRANSIENT_TRANSPORT:
            status_str = "TRANSIENT_TRANSPORT: A transient transport error was detected."
        elif status == NvmeStatus.NVME_SC_CAP_EXCEEDED:
            status_str = "CAP_EXCEEDED: The execution of the command has caused the capacity of the namespace to be exceeded"
        elif status == NvmeStatus.NVME_SC_NS_NOT_READY:
            status_str = "NS_NOT_READY: The namespace is not ready to be accessed as a result of a condition other than a condition that is reported as an Asymmetric Namespace Access condition"
        elif status == NvmeStatus.NVME_SC_RESERVATION_CONFLICT:
            status_str = "RESERVATION_CONFLICT: The command was aborted due to a conflict with a reservation held on the accessed namespace"
        elif status == NvmeStatus.NVME_SC_FORMAT_IN_PROGRESS:
            status_str = "FORMAT_IN_PROGRESS: A Format NVM command is in progress on the namespace."
        elif status == NvmeStatus.NVME_SC_ZONE_BOUNDARY_ERROR:
            status_str = "ZONE_BOUNDARY_ERROR: Invalid Zone Boundary crossing"
        elif status == NvmeStatus.NVME_SC_ZONE_IS_FULL:
            status_str = "ZONE_IS_FULL: The accessed zone is in ZSF:Full state"
        elif status == NvmeStatus.NVME_SC_ZONE_IS_READ_ONLY:
            status_str = "ZONE_IS_READ_ONLY: The accessed zone is in ZSRO:Read Only state"
        elif status == NvmeStatus.NVME_SC_ZONE_IS_OFFLINE:
            status_str = "ZONE_IS_OFFLINE: The access zone is in ZSO:Offline state"
        elif status == NvmeStatus.NVME_SC_ZONE_INVALID_WRITE:
            status_str = "ZONE_INVALID_WRITE: The write to zone was not at the write pointer offset"
        elif status == NvmeStatus.NVME_SC_TOO_MANY_ACTIVE_ZONES:
            status_str = "TOO_MANY_ACTIVE_ZONES: The controller does not allow additional active zones"
        elif status == NvmeStatus.NVME_SC_TOO_MANY_OPEN_ZONES:
            status_str = "TOO_MANY_OPEN_ZONES: The controller does not allow additional open zones"
        elif status == NvmeStatus.NVME_SC_ZONE_INVALID_STATE_TRANSITION:
            status_str = "INVALID_ZONE_STATE_TRANSITION: The zone state change was invalid"
        elif status == NvmeStatus.NVME_SC_CQ_INVALID:
            status_str = "CQ_INVALID: The Completion Queue identifier specified in the command does not exist"
        elif status == NvmeStatus.NVME_SC_QID_INVALID:
            status_str = "QID_INVALID: The creation of the I/O Completion Queue failed due to an invalid queue identifier specified as part of the command. An invalid queue identifier is one that is currently in use or one that is outside the range supported by the controller"
        elif status == NvmeStatus.NVME_SC_QUEUE_SIZE:
            status_str = "QUEUE_SIZE: The host attempted to create an I/O Completion Queue with an invalid number of entries"
        elif status == NvmeStatus.NVME_SC_ABORT_LIMIT:
            status_str = "ABORT_LIMIT: The number of concurrently outstanding Abort commands has exceeded the limit indicated in the Identify Controller data structure"
        elif status == NvmeStatus.NVME_SC_ABORT_MISSING:
            status_str = "ABORT_MISSING: The abort command is missing"
        elif status == NvmeStatus.NVME_SC_ASYNC_LIMIT:
            status_str = "ASYNC_LIMIT: The number of concurrently outstanding Asynchronous Event Request commands has been exceeded"
        elif status == NvmeStatus.NVME_SC_FIRMWARE_SLOT:
            status_str = "FIRMWARE_SLOT: The firmware slot indicated is invalid or read only. This error is indicated if the firmware slot exceeds the number supported"
        elif status == NvmeStatus.NVME_SC_FIRMWARE_IMAGE:
            status_str = "FIRMWARE_IMAGE: The firmware image specified for activation is invalid and not loaded by the controller"
        elif status == NvmeStatus.NVME_SC_INVALID_VECTOR:
            status_str = "INVALID_VECTOR: The creation of the I/O Completion Queue failed due to an invalid interrupt vector specified as part of the command"
        elif status == NvmeStatus.NVME_SC_INVALID_LOG_PAGE:
            status_str = "INVALID_LOG_PAGE: The log page indicated is invalid. This error condition is also returned if a reserved log page is requested"
        elif status == NvmeStatus.NVME_SC_INVALID_FORMAT:
            status_str = "INVALID_FORMAT: The LBA Format specified is not supported. This may be due to various conditions"
        elif status == NvmeStatus.NVME_SC_FW_NEEDS_CONV_RESET:
            status_str = "FW_NEEDS_CONVENTIONAL_RESET: The firmware commit was successful, however, activation of the firmware image requires a conventional reset"
        elif status == NvmeStatus.NVME_SC_INVALID_QUEUE:
            status_str = "INVALID_QUEUE: This error indicates that it is invalid to delete the I/O Completion Queue specified. The typical reason for this error condition is that there is an associated I/O Submission Queue that has not been deleted."
        elif status == NvmeStatus.NVME_SC_FEATURE_NOT_SAVEABLE:
            status_str = "FEATURE_NOT_SAVEABLE: The Feature Identifier specified does not support a saveable value"
        elif status == NvmeStatus.NVME_SC_FEATURE_NOT_CHANGEABLE:
            status_str = "FEATURE_NOT_CHANGEABLE: The Feature Identifier is not able to be changed"
        elif status == NvmeStatus.NVME_SC_FEATURE_NOT_PER_NS:
            status_str = "FEATURE_NOT_PER_NS: The Feature Identifier specified is not namespace specific. The Feature Identifier settings apply across all namespaces"
        elif status == NvmeStatus.NVME_SC_FW_NEEDS_SUBSYS_RESET:
            status_str = "FW_NEEDS_SUBSYSTEM_RESET: The firmware commit was successful, however, activation of the firmware image requires an NVM Subsystem"
        elif status == NvmeStatus.NVME_SC_FW_NEEDS_RESET:
            status_str = "FW_NEEDS_RESET: The firmware commit was successful; however, the image specified does not support being activated without a reset"
        elif status == NvmeStatus.NVME_SC_FW_NEEDS_MAX_TIME:
            status_str = "FW_NEEDS_MAX_TIME_VIOLATION: The image specified if activated immediately would exceed the Maximum Time for Firmware Activation (MTFA) value reported in Identify Controller. To activate the firmware, the Firmware Commit command needs to be re-issued and the image activated using a reset"
        elif status == NvmeStatus.NVME_SC_FW_ACTIVATE_PROHIBITED:
            status_str = "FW_ACTIVATION_PROHIBITED: The image specified is being prohibited from activation by the controller for vendor specific reasons"
        elif status == NvmeStatus.NVME_SC_OVERLAPPING_RANGE:
            status_str = "OVERLAPPING_RANGE: This error is indicated if the firmware image has overlapping ranges"
        elif status == NvmeStatus.NVME_SC_NS_INSUFFICIENT_CAP:
            status_str = "NS_INSUFFICIENT_CAPACITY: Creating the namespace requires more free space than is currently available. The Command Specific Information field of the Error Information Log specifies the total amount of NVM capacity required to create the namespace in bytes"
        elif status == NvmeStatus.NVME_SC_NS_ID_UNAVAILABLE:
            status_str = "NS_ID_UNAVAILABLE: The number of namespaces supported has been exceeded"
        elif status == NvmeStatus.NVME_SC_NS_ALREADY_ATTACHED:
            status_str = "NS_ALREADY_ATTACHED: The controller is already attached to the namespace specified"
        elif status == NvmeStatus.NVME_SC_NS_IS_PRIVATE:
            status_str = "NS_IS_PRIVATE: The namespace is private and is already attached to one controller"
        elif status == NvmeStatus.NVME_SC_NS_NOT_ATTACHED:
            status_str = "NS_NOT_ATTACHED: The request to detach the controller could not be completed because the controller is not attached to the namespace"
        elif status == NvmeStatus.NVME_SC_THIN_PROV_NOT_SUPP:
            status_str = "THIN_PROVISIONING_NOT_SUPPORTED: Thin provisioning is not supported by the controller"
        elif status == NvmeStatus.NVME_SC_CTRL_LIST_INVALID:
            status_str = "CONTROLLER_LIST_INVALID: The controller list provided is invalid"
        elif status == NvmeStatus.NVME_SC_DEVICE_SELF_TEST_IN_PROGRESS:
            status_str = "DEVICE_SELF_TEST_IN_PROGRESS: The controller or NVM subsystem already has a device self-test operation in process."
        elif status == NvmeStatus.NVME_SC_BP_WRITE_PROHIBITED:
            status_str = "BOOT PARTITION WRITE PROHIBITED: The command is trying to modify a Boot Partition while it is locked"
        elif status == NvmeStatus.NVME_SC_INVALID_CTRL_ID:
            status_str = "INVALID_CTRL_ID: An invalid Controller Identifier was specified."
        elif status == NvmeStatus.NVME_SC_INVALID_SECONDARY_CTRL_STATE:
            status_str = "INVALID_SECONDARY_CTRL_STATE: The action requested for the secondary controller is invalid based on the current state of the secondary controller and its primary controller."
        elif status == NvmeStatus.NVME_SC_INVALID_NUM_CTRL_RESOURCE:
            status_str = "INVALID_NUM_CTRL_RESOURCE: The specified number of Flexible Resources is invalid"
        elif status == NvmeStatus.NVME_SC_INVALID_RESOURCE_ID:
            status_str = "INVALID_RESOURCE_ID: At least one of the specified resource identifiers was invalid"
        elif status == NvmeStatus.NVME_SC_ANA_INVALID_GROUP_ID:
            status_str = "ANA_INVALID_GROUP_ID: The specified ANA Group Identifier (ANAGRPID) is not supported in the submitted command."
        elif status == NvmeStatus.NVME_SC_ANA_ATTACH_FAIL:
            status_str = "ANA_ATTACH_FAIL: The controller is not attached to the namespace as a result of an ANA condition"
        elif status == NvmeStatus.NVME_SC_BAD_ATTRIBUTES:
            status_str = "BAD_ATTRIBUTES: Bad attributes were given"
        elif status == NvmeStatus.NVME_SC_INVALID_PI:
            status_str = "INVALID_PROTECION_INFO: The Protection Information Field settings specified in the command are invalid"
        elif status == NvmeStatus.NVME_SC_READ_ONLY:
            status_str = "WRITE_ATTEMPT_READ_ONLY_RANGE: The LBA range specified contains read-only blocks"
        elif status == NvmeStatus.NVME_SC_CMD_SIZE_LIMIT_EXCEEDED:
            status_str = "CMD_SIZE_LIMIT_EXCEEDED: Command size limit exceeded"
        elif status == NvmeStatus.NVME_SC_WRITE_FAULT:
            status_str = "WRITE_FAULT: The write data could not be committed to the media"
        elif status == NvmeStatus.NVME_SC_READ_ERROR:
            status_str = "READ_ERROR: The read data could not be recovered from the media"
        elif status == NvmeStatus.NVME_SC_GUARD_CHECK:
            status_str = "GUARD_CHECK: The command was aborted due to an end-to-end guard check failure"
        elif status == NvmeStatus.NVME_SC_APPTAG_CHECK:
            status_str = "APPTAG_CHECK: The command was aborted due to an end-to-end application tag check failure"
        elif status == NvmeStatus.NVME_SC_REFTAG_CHECK:
            status_str = "REFTAG_CHECK: The command was aborted due to an end-to-end reference tag check failure"
        elif status == NvmeStatus.NVME_SC_COMPARE_FAILED:
            status_str = "COMPARE_FAILED: The command failed due to a miscompare during a Compare command"
        elif status == NvmeStatus.NVME_SC_ACCESS_DENIED:
            status_str = "ACCESS_DENIED: Access to the namespace and/or LBA range is denied due to lack of access rights"
        elif status == NvmeStatus.NVME_SC_UNWRITTEN_BLOCK:
            status_str = "UNWRITTEN_BLOCK: The command failed due to an attempt to read from an LBA range containing a deallocated or unwritten logical block"
        elif status == NvmeStatus.NVME_SC_INTERNAL_PATH_ERROR:
            status_str = "INTERNAL_PATH_ERROT: The command was not completed as the result of a controller internal error"
        elif status == NvmeStatus.NVME_SC_ANA_PERSISTENT_LOSS:
            status_str = "ASYMMETRIC_NAMESPACE_ACCESS_PERSISTENT_LOSS: The requested function (e.g., command) is not able to be performed as a result of the relationship between the controller and the namespace being in the ANA Persistent Loss state"
        elif status == NvmeStatus.NVME_SC_ANA_INACCESSIBLE:
            status_str = "ASYMMETRIC_NAMESPACE_ACCESS_INACCESSIBLE: The requested function (e.g., command) is not able to be performed as a result of the relationship between the controller and the namespace being in the ANA Inaccessible state"
        elif status == NvmeStatus.NVME_SC_ANA_TRANSITION:
            status_str = "ASYMMETRIC_NAMESPACE_ACCESS_TRANSITION: The requested function (e.g., command) is not able to be performed as a result of the relationship between the controller and the namespace transitioning between Asymmetric Namespace Access states"
        elif status == NvmeStatus.NVME_SC_CTRL_PATHING_ERROR:
            status_str = "CONTROLLER_PATHING_ERROR: A pathing error was detected by the controller"
        elif status == NvmeStatus.NVME_SC_HOST_PATHING_ERROR:
            status_str = "HOST_PATHING_ERROR: A pathing error was detected by the host"
        elif status == NvmeStatus.NVME_SC_HOST_CMD_ABORT:
            status_str = "HOST_COMMAND_ABORT: The command was aborted as a result of host action"
        elif status == NvmeStatus.NVME_SC_CMD_INTERRUPTED:
            status_str = "CMD_INTERRUPTED: Command processing was interrupted and the controller is unable to successfully complete the command. The host should retry the command."
        elif status == NvmeStatus.NVME_SC_PMR_SAN_PROHIBITED:
            status_str = "Sanitize Prohibited While Persistent Memory Region is Enabled: A sanitize operation is prohibited while the Persistent Memory Region is enabled."
        else:
            status_str = "Unknown"

        # return str(self.status)+f': ({status_str})'
        return status_str

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return f'{self.status}: ({self.status_str})'

    def __getstate__(self, all_info=True):
        """
        Allows us to send a pySMART diagnostics object over a serializable
        medium which uses json (or the likes of json) payloads
        """
        return vars(self)

    def __setstate__(self, state):
        self.__dict__.update(state)


class NvmeSelfTest(object):
    """This class represents a test entry of a NVMe device

    Attributes:
        num (int): The test number
        description (str): The test description
        status (str): The test status
        powerOnHours (int): The power on hours
        failingLBA (Optional[int]): The failing LBA
        nsid (Optional[int]): The namespace ID
        seg (Optional[int]): Manufacturer-specific self-test segment number
        sct (Optional[str]): The SCT
        code (Optional[str]): The code
        progress (int): The progress of the test. Defaults to 100%

    """

    def __init__(self, num: int, description: str, status: str, powerOnHours: int, failingLBA: Optional[int] = None, nsid: Optional[int] = None, seg: Optional[int] = None, sct: Optional[str] = None, code: Optional[str] = None, progress: int = 100):

        self.num: int = num
        self.description: str = description
        self.status: str = status
        self.powerOnHours: int = powerOnHours
        self.failingLBA: Optional[int] = failingLBA
        self.nsid: Optional[int] = nsid
        self.seg: Optional[int] = seg
        self.sct: Optional[str] = sct
        self.code: Optional[str] = code
        self.progress: int = progress

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        # Example smartctl output
        # Self-test Log (NVMe Log 0x06)
        # Self-test status: Extended self-test in progress (28% completed)
        # Num  Test_Description  Status                       Power_on_Hours  Failing_LBA  NSID Seg SCT Code
        #  0   Extended          Completed without error                3441            -     -   -   -    -
        if self.seg is not None:
            # for backwards compatibility
            return ("{0:>2} {1:18}{2:29}{3:16}{4:13}{5:5}{6:4}{7:4}".format(
                self.num,
                self.description,
                self.status,
                self.powerOnHours,
                self.failingLBA,
                self.nsid,
                self.sct,
                self.code
            ))

        return ("{0:>2} {1:18}{2:29}{3:16}{4:>13}{5:>5}{6:>4}{7:>4}{8:>4}".format(
            self.num,
            self.description,
            self.status,
            self.powerOnHours,
            self.failingLBA,
            self.nsid,
            self.seg,
            self.sct,
            self.code
        ))

    def __getstate__(self, all_info=True):
        """
        Allows us to send a pySMART diagnostics object over a serializable
        medium which uses json (or the likes of json) payloads
        """
        return vars(self)

    def __setstate__(self, state):
        self.__dict__.update(state)


class NvmeAttributes(CommonIface):
    """This class represents the attributes of a NVMe device

    Attributes:
        criticalWarning          : Number of critical warnings
        temperature              : Temperature in Celsius
        availableSpare           : Available spare in percentage
        availableSpareThreshold  : Available spare threshold in percentage
        percentageUsed           : Data units used in percentage
        dataUnitsRead            : Data units (sectors) read
        bytesRead                : Bytes read
        dataUnitsWritten         : Data units (sectors) written
        bytesWritten             : Bytes written
        hostReadCommands         : Host read commands
        hostWriteCommands        : Host write commands
        controllerBusyTime       : Controller busy time in minutes
        powerCycles              : Power on cycles
        powerOnHours             : Power on hours
        unsafeShutdowns          : Unsafe shutdowns
        integrityErrors          : Integrity errors
        errorEntries             : Error log entries
        warningTemperatureTime   : Time in minutes at warning temperature
        criticalTemperatureTime  : Time in minutes at critical temperature

        _logical_sector_size     : Logical sector size
        _physical_sector_size    : Physical sector size

        errors                   : List of errors
        tests                    : List of self-tests
    """

    def __init__(self, data: Optional[Iterator[str]] = None):
        """Initializes the attributes

        Args:
            data (Iterator[str], optional): Iterator of the lines of the output of the command nvme smart-log. Defaults to None.

        """

        self.criticalWarning: Optional[int] = None
        self._temperature: Optional[int] = None
        self.availableSpare: Optional[int] = None
        self.availableSpareThreshold: Optional[int] = None
        self.percentageUsed: Optional[int] = None
        self.dataUnitsRead: Optional[int] = None
        self.bytesRead: Optional[int] = None
        self.dataUnitsWritten: Optional[int] = None
        self.bytesWritten: Optional[int] = None
        self.hostReadCommands: Optional[int] = None
        self.hostWriteCommands: Optional[int] = None
        self.controllerBusyTime: Optional[int] = None
        self.powerCycles: Optional[int] = None
        self.powerOnHours: Optional[int] = None
        self.unsafeShutdowns: Optional[int] = None
        self.integrityErrors: Optional[int] = None
        self.errorEntries: Optional[int] = None
        self.warningTemperatureTime: Optional[int] = None
        self.criticalTemperatureTime: Optional[int] = None

        self._logical_sector_size: Optional[int] = None
        self._physical_sector_size: Optional[int] = None

        self.errors: List[NvmeError] = []
        self.tests: List[NvmeSelfTest] = []

        if data is not None:
            self.parse(data)

    def parse(self, data: Iterator[str]) -> None:
        """Parses the attributes from the raw data
        """

        # Advance data until detect Nvme Log
        for line in data:

            # Sector sizes
            if 'Sector Sizes' in line:  # ATA
                m = re.match(
                    r'.* (\d+) bytes logical,\s*(\d+) bytes physical', line)
                if m:
                    self._logical_sector_size = int(m.group(1))
                    self._physical_sector_size = int(m.group(2))
                continue
            if 'Logical block size:' in line:  # SCSI 1/2
                self._logical_sector_size = int(
                    line.split(':')[1].strip().split(' ')[0])
                continue
            if 'Physical block size:' in line:  # SCSI 2/2
                self._physical_sector_size = int(
                    line.split(':')[1].strip().split(' ')[0])
                continue
            if 'Namespace 1 Formatted LBA Size' in line:  # NVMe
                # Note: we will assume that there is only one namespace
                self._logical_sector_size = int(
                    line.split(':')[1].strip().split(' ')[0])
                continue

            # Smart section: 'SMART/Health Information (NVMe Log 0x02)'
            if line.startswith('SMART/Health Information (NVMe Log 0x02)'):

                # Parse attributes
                for line in data:
                    line = line.strip()

                    if not line or len(line) == 0:
                        break

                    # Parse attribute
                    match = re.match(
                        r'^\s*(?P<name>.+)\s*:\s*(?P<value>.+)\s*$', line)
                    if match:
                        name = match.group('name')
                        value = match.group('value')

                        if name == 'Critical Warning':
                            self.criticalWarning = int(value, 16)
                        elif name == 'Temperature':
                            # Check if temperature is in Celsius or Fahrenheit
                            if value.endswith('Celsius'):
                                self._temperature = int(value[:-7])
                            elif value.endswith('Fahrenheit'):
                                self._temperature = int(
                                    (int(value[:-10]) - 32) / 1.8)
                        elif name == 'Available Spare':
                            self.availableSpare = int(value[:-1])
                        elif name == 'Available Spare Threshold':
                            self.availableSpareThreshold = int(value[:-1])
                        elif name == 'Percentage Used':
                            self.percentageUsed = int(value[:-1])
                        elif name == 'Data Units Read':
                            # Format: 1,234,567 [2.00 TB]
                            # Or    : 0
                            if value.isdigit():
                                self.dataUnitsRead = int(value)
                                self.bytesRead = int(value)
                            else:
                                self.dataUnitsRead = int(
                                    value.split(' ')[0].replace(',', '').replace('.', '').replace('’', ''))
                                self.bytesRead = humanfriendly.parse_size(
                                    value.split(' ', 1)[1][1:-1].replace(',', '.'))
                        elif name == 'Data Units Written':
                            # Format: 1,234,567 [2.00 TB]
                            # Or    : 0
                            if value.isdigit():
                                self.dataUnitsWritten = int(value)
                                self.bytesWritten = int(value)
                            else:
                                self.dataUnitsWritten = int(
                                    value.split(' ')[0].replace(',', '').replace('.', '').replace('’', ''))
                                self.bytesWritten = humanfriendly.parse_size(
                                    value.split(' ', 1)[1][1:-1].replace(',', '.'))
                        elif name == 'Host Read Commands':
                            self.hostReadCommands = int(
                                value.replace(',', '').replace('.', '').replace('’', ''))
                        elif name == 'Host Write Commands':
                            self.hostWriteCommands = int(
                                value.replace(',', '').replace('.', '').replace('’', ''))
                        elif name == 'Controller Busy Time':
                            self.controllerBusyTime = int(
                                value.replace(',', '').replace('.', '').replace('’', ''))
                        elif name == 'Power Cycles':
                            self.powerCycles = int(
                                value.replace(',', '').replace('.', ''))
                        elif name == 'Power On Hours':
                            self.powerOnHours = int(
                                value.replace(',', '').replace('.', '').replace('’', ''))
                        elif name == 'Unsafe Shutdowns':
                            self.unsafeShutdowns = int(
                                value.replace(',', '').replace('.', ''))
                        elif name == 'Media and Data Integrity Errors':
                            self.integrityErrors = int(
                                value.replace(',', '').replace('.', ''))
                        elif name == 'Error Information Log Entries':
                            self.errorEntries = int(
                                value.replace(',', '').replace('.', ''))
                        elif name == 'Warning Comp. Temperature Time':
                            self.warningTemperatureTime = int(
                                value.replace(',', '').replace('.', ''))
                        elif name == 'Critical Comp. Temperature Time':
                            self.criticalTemperatureTime = int(
                                value.replace(',', '').replace('.', ''))

            # Smart section: Error Information (NVMe Log 0x01, <num_entries> of <max_entries> entries)
            elif line.startswith('Error Information (NVMe Log 0x01, '):

                # check next line is:
                # Num   ErrCount  SQId   CmdId  Status  PELoc          LBA  NSID    VS
                # but be careful with the spaces

                line = next(data)
                if not re.match(r'^\s*Num\s+ErrCount\s+SQId\s+CmdId\s+Status\s+PELoc\s+LBA\s+NSID\s+VS\s*$', line):
                    continue

                # Parse errors
                for line in data:
                    line = line.strip()

                    if not line or len(line) == 0:
                        break

                    # Parse error
                    # Format:    Num   ErrCount  SQId   CmdId  Status  PELoc          LBA  NSID    VS
                    # example 1:   0       1356     0  0x0012  0xc005  0x028            -     0     -
                    # example 2:   3          1     3  0x0045  0xc006  0x049           56     3     2

                    match = re.match(
                        r'^\s*(?P<num>\d+)\s+(?P<errCount>\d+)\s+(?P<sqId>\d+)\s+(?P<cmdId>\w+)\s+(?P<status>\w+)\s+(?P<peLoc>\w+)\s+(?P<lba>\S+)\s+(?P<nsid>\S+)\s+(?P<vs>\S+)\s*$', line)

                    if match:
                        error = NvmeError(
                            num=int(match.group('num')),
                            errCount=int(match.group('errCount')),
                            sqId=int(match.group('sqId')),
                            cmdId=int(match.group('cmdId'), 16),
                            status=int(match.group('status'), 16),
                            peLoc=int(match.group('peLoc'), 16)
                        )

                        if match.group('lba') != '-':
                            error.lba = int(match.group('lba'), 16)

                        if match.group('nsid') != '-':
                            error.nsid = int(match.group('nsid'))

                        if match.group('vs') != '-':
                            error.vs = int(match.group('vs'), 16)

                        self.errors.append(error)

            elif line.startswith('Self-test Log (NVMe Log 0x06)'):

                ## NVME FORMAT ##
                # Example smartctl output
                # Self-test Log (NVMe Log 0x06)
                # Self-test status: Extended self-test in progress (28% completed)
                # Num  Test_Description  Status                       Power_on_Hours  Failing_LBA  NSID Seg SCT Code
                #  0   Extended          Completed without error                3441            -     -   -   -    -
                nvme_entry_regex = re.compile(
                    r'^[#\s]*(\d+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(\d+)\s{2,}(.*?[^\s])\s{2,}(.*?[^\s])(?:\s{2,}(.*[^\s]))?\s{2,}(.*[^\s])\s{2,}(.*)$')

                line = next(data)

                # Check the current test
                # Example of non running test:  Self-test status: No self-test in progress
                # Example of running test:      Self-test status: Extended self-test in progress (4% completed)
                currentTest = None
                if line.startswith('Self-test status:'):
                    line = line[18:].strip()

                    # check
                    if line.startswith('No self-test in progress'):
                        pass
                    else:
                        # parse the test
                        match = re.match(
                            r'^(\w+) self-test in progress \((\d+)% completed\)$', line)
                        if match:
                            powerOnHours = self.powerOnHours
                            if powerOnHours is None:
                                powerOnHours = 0

                            currentTest = NvmeSelfTest(
                                num=-1,
                                description=match.group(1),
                                status='Running',
                                powerOnHours=powerOnHours,
                                progress=int(match.group(2))
                            )

                            self.tests.append(currentTest)

                # Parse tests
                for line in data:
                    line = line.strip()

                    match = nvme_entry_regex.match(line)

                    if match:

                        num = int(match.group(1))
                        description = match.group(2)
                        status = match.group(3)
                        powerOnHours = int(match.group(4))

                        failingLBA = None
                        if match.group(5) != '-':
                            failingLBA = int(match.group(5))

                        nsid = None
                        if match.group(6) != '-':
                            nsid = int(match.group(6))

                        seg = None
                        if match.group(7) is not None and match.group(7) != '-':
                            seg = int(match.group(7))

                        sct = match.group(8)
                        code = match.group(9)

                        test = NvmeSelfTest(
                            num=num,
                            description=description,
                            status=status,
                            powerOnHours=powerOnHours,
                            failingLBA=failingLBA,
                            nsid=nsid,
                            seg=seg,
                            sct=sct,
                            code=code
                        )

                        self.tests.append(test)

    def __getstate__(self):
        """
        Allows us to send a pySMART diagnostics object over a serializable
        medium which uses json (or the likes of json) payloads
        """
        import copy
        ret = copy.copy(vars(self))

        if ret['errors'] is not None:
            ret['errors'] = [e.__getstate__() for e in ret['errors']]

        if ret['tests'] is not None:
            ret['tests'] = [e.__getstate__() for e in ret['tests']]

        return ret

    def __setstate__(self, state):
        self.__dict__.update(state)

    @property
    def temperature(self) -> Optional[int]:
        return self._temperature

    @property
    def physical_sector_size(self) -> int:
        if self._physical_sector_size is not None:
            return self._physical_sector_size
        elif self._logical_sector_size is not None:
            return self._logical_sector_size
        else:
            return 512

    @property
    def logical_sector_size(self) -> int:
        return self._logical_sector_size if self._logical_sector_size is not None else self.physical_sector_size
