# Copyright (c) 2014 The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import struct
from typing import Sequence, Tuple
from typing_extensions import TypeAlias
from spinnman.model.enums import CPUState, RunTimeError, MailboxCommand

#: Size of `vcpu_t` in SARK.
CPU_INFO_BYTES = 128
CPU_USER_0_START_ADDRESS = 112
#: Offset into data of byte of processor state field.
STATE_FIELD_OFFSET = 48

#: Corresponds to vcpu_t in sark.h
_VCPU_PATTERN = struct.Struct("< 32s 3I 2B 2B 2I 2B H 3I 16s 2I 16x 4I")
_vcpu_t: TypeAlias = Tuple[
    # pylint: disable=wrong-spelling-in-comment
    bytes,             # 32s - r0-r7
    int, int, int,     # 3I  - psr, sp, lr
    int, int,          # 2B  - RT error code, Physical CPU
    int, int,          # 2B  - CPU state, Application ID
    int, int,          # 2I  - mbox msg MP->AP, mbox msg AP->MP
    int, int,          # 2B  - mbox command MP->AP, mbox command AP->MP
    int,               # H   - SW error count (saturating)
    int, int, int,     # 3I  - SW source filename, Source line, Time of loading
    bytes,             # 16s - Application name
    int, int,          # 2I  - IO buffer in SDRAM (or 0)
    #                    16x - Padding
    int, int, int, int]  # 4I- User0, User1, User2, User3
_REGISTERS_PATTERN = struct.Struct("<IIIIIIII")


class CPUInfo(object):
    """
    Represents information about the state of a CPU.

    This is the content of the `vcpu_t` for the processor, maintained by SARK.
    """
    __slots__ = [
        "__application_id",
        "__application_mailbox_command",
        "__app_mailbox",
        "__application_name",
        "__iobuf_address",
        "__link_register",
        "__monitor_mailbox_command",
        "__monitor_mailbox",
        "__physical_cpu_id",
        "__processor_state_register",
        "__registers",
        "__run_time_error",
        "__run_time_error_value",
        "__software_error_count",
        "__filename_address",
        "__line_number",
        "__software_version",
        "__stack_pointer",
        "__state",
        "__time",
        "__user",
        "__x", "__y", "__p"]

    def __init__(self, x: int, y: int, p: int, cpu_data: _vcpu_t):
        """
        :param int x: The x-coordinate of a chip
        :param int y: The y-coordinate of a chip
        :param int p: The ID of a core on the chip
        :param tuple cpu_data: A byte-string received from SDRAM on the board
        """
        # pylint: disable=too-many-arguments
        self.__x, self.__y, self.__p = x, y, p

        (registers,  # 32s 0
         self.__processor_state_register, self.__stack_pointer,
         self.__link_register,  run_time_error,
         self.__physical_cpu_id,  # 2B  44
         state, self.__application_id,  # 2B  46
         self.__app_mailbox, self.__monitor_mailbox,  # 2I  48
         app_mailbox_cmd, mon_mailbox_cmd,  # 2B  56
         self.__software_error_count,  # H   58
         self.__filename_address, self.__line_number, self.__time,  # 3I  60
         app_name,  # 16s 72
         self.__iobuf_address, self.__software_version,  # 2I  88
         # skipped                                                  # 16x 96
         user0, user1, user2, user3                                 # 4I  112
         ) = cpu_data

        index = app_name.find(b'\0')
        if index != -1:
            app_name = app_name[:index]
        self.__application_name = app_name.decode('ascii')

        self.__registers: Sequence[int] = _REGISTERS_PATTERN.unpack(registers)
        self.__run_time_error_value = run_time_error
        try:
            self.__run_time_error = RunTimeError(run_time_error)
        except ValueError:
            self.__run_time_error = RunTimeError.UNRECOGNISED
        self.__state = CPUState(state)

        self.__application_mailbox_command = MailboxCommand(app_mailbox_cmd)
        self.__monitor_mailbox_command = MailboxCommand(mon_mailbox_cmd)
        self.__user = (user0, user1, user2, user3)

    @property
    def x(self) -> int:
        """
        The X-coordinate of the chip containing the core.

        :return: The x-coordinate of the chip
        :rtype: int
        """
        return self.__x

    @property
    def y(self) -> int:
        """
        The y-coordinate of the chip containing the core.

        :return: The y-coordinate of the chip
        :rtype: int
        """
        return self.__y

    @property
    def p(self) -> int:
        """
        The ID of the core on the chip.

        :return: The ID of the core
        :rtype: int
        """
        return self.__p

    @property
    def state(self) -> CPUState:
        """
        The current state of the core.

        :return: The state of the core
        :rtype: CPUState
        """
        return self.__state

    @property
    def physical_cpu_id(self) -> int:
        """
        The physical ID of this processor.

        :return: The physical ID of the processor
        :rtype: int
        """
        return self.__physical_cpu_id

    @property
    def application_name(self) -> str:
        """
        The name of the application running on the core.

        :return: The name of the application
        :rtype: str
        """
        return self.__application_name

    @property
    def application_id(self) -> int:
        """
        The ID of the application running on the core.

        :return: The ID of the application
        :rtype: int
        """
        return self.__application_id

    @property
    def time(self) -> int:
        """
        The time at which the application started.

        :return: The time in seconds since 00:00:00 on the 1st January 1970
        :rtype: int
        """
        return self.__time

    @property
    def run_time_error(self) -> RunTimeError:
        """
        The reason for a run time error.

        :return: The run time error
        :rtype: RunTimeError
        """
        return self.__run_time_error

    @property
    def application_mailbox_command(self) -> MailboxCommand:
        """
        The command currently in the mailbox being sent from the monitor
        processor to the application.

        :return: The command
        :rtype: MailboxCommand
        """
        return self.__application_mailbox_command

    @property
    def application_mailbox_data_address(self) -> int:
        """
        The address of the data in SDRAM for the application mailbox.

        :return: The address of the data
        :rtype: int
        """
        return self.__app_mailbox

    @property
    def monitor_mailbox_command(self) -> MailboxCommand:
        """
        The command currently in the mailbox being sent from the
        application to the monitor processor.

        :return: The command
        :rtype: MailboxCommand
        """
        return self.__monitor_mailbox_command

    @property
    def monitor_mailbox_data_address(self) -> int:
        """
        The address of the data in SDRAM of the monitor mailbox.

        :return: The address of the data
        :rtype: int
        """
        return self.__monitor_mailbox

    @property
    def software_error_count(self) -> int:
        """
        The number of software errors counted. Saturating.

        :return: The number of software errors
        :rtype: int
        """
        return self.__software_error_count

    @property
    def software_source_filename_address(self) -> int:
        """
        The address of the filename of the software source.

        :return: The filename address
        :rtype: int
        """
        return self.__filename_address

    @property
    def software_source_line_number(self) -> int:
        """
        The line number of the software source.

        :return: The line number
        :rtype: int
        """
        return self.__line_number

    @property
    def processor_state_register(self) -> int:
        """
        The value in the processor state register.

        :return: The processor state register value
        :rtype: int
        """
        return self.__processor_state_register

    @property
    def stack_pointer(self) -> int:
        """
        The current stack pointer value.

        :return: The stack pointer value
        :rtype: int
        """
        return self.__stack_pointer

    @property
    def link_register(self) -> int:
        """
        The current link register value.

        :return: The link register value
        :rtype: int
        """
        return self.__link_register

    @property
    def registers(self) -> Sequence[int]:
        """
        The current register values (r0 - r7).

        :return: An array of 8 values, one for each register
        :rtype: list(int)
        """
        return self.__registers

    @property
    def user(self) -> Sequence[int]:
        """
        The current user values (user0 - user3).

        :return: An array of 4 values, one for each user value
        :rtype: list(int)
        """
        return self.__user

    @property
    def iobuf_address(self) -> int:
        """
        The address of the IOBUF buffer in SDRAM.

        :return: The address
        :rtype: int
        """
        return self.__iobuf_address

    @property
    def software_version(self) -> int:
        """
        The software version.

        :return: The software version
        :rtype: int
        """
        return self.__software_version

    def __str__(self) -> str:
        return (f"{self.x}:{self.y}:{self.p:02n} ({self.physical_cpu_id:02n}) "
                f"{self.__state.name:18} {self.__application_name:16s} "
                f"{self.__application_id:3n}")

    def get_status_string(self) -> str:
        """
        Get a string indicating the status of the given core.

        :rtype: str
        """
        if self.state == CPUState.RUN_TIME_EXCEPTION:
            rte_string = f"{self.run_time_error.name}"
            if self.run_time_error == RunTimeError.UNRECOGNISED:
                rte_string += f" ({self.__run_time_error_value})"
            return (
                f"{self.__x}:{self.__y}:{self.__p} "
                f"(ph: {self.__physical_cpu_id}) "
                f"in state {self.__state.name}:{rte_string}\n"
                f"    r0={self.__registers[0]}, r1={self.__registers[1]}, "
                f"r2={self.__registers[2]}, r3={self.__registers[3]}\n"
                f"    r4={self.__registers[4]}, r5={self.__registers[5]}, "
                f"r6={self.__registers[6]}, r7={self.__registers[7]}\n"
                f"    PSR={self.__processor_state_register}, "
                f"SP={self.__stack_pointer}, LR={self.__link_register}\n")
        else:
            return (
                f"{self.__x}:{self.__y}:{self.__p} "
                f"in state {self.__state.name}\n")

    @staticmethod
    def mock_info(
            x: int, y: int, p: int, physical_cpu_id: int, state: CPUState):
        """
        Makes a CPU_info object for Testing purposes

        :param int x:
        :param int y:
        :param int p:
        :param int physical_cpu_id:
        :param CPUState CPIstate:
        """
        registers = b'@\x00\x07\x08\xff\x00\x00\x00\x00\x00\x80\x00\xad\x00' \
                    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                    b'\x00\x00\x00\x00\x00'
        time = 1687857627
        application_name = b'scamp-3\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        iobuff_address = 197634
        cpu_data = (
            registers, 0, 0, 0, 0, physical_cpu_id, state.value, 0, 0, 0, 0,
            0, 0, 0, 0, time, application_name, iobuff_address, 0, 0, 0, 0, 0)
        return CPUInfo(x, y, p, cpu_data)
