"""
Datastructure definitions used in the C ebpf code.
"""
from __future__ import annotations

import ctypes as ct
from enum import IntEnum
from typing import Dict, List, Tuple, Type

from ..unwind import stack_data_t

BPF_MAP_TYPE_QUEUE = 22


class Id128(ct.Structure):
    """
    Structure containing two u64, to be used as either a single 8-bytes int or a two 8-bytes tuple.
    """

    _fields_ = [("u1", ct.c_ulonglong), ("u2", ct.c_ulonglong)]

    @classmethod
    def from_int(cls, intvalue: int) -> Id128:
        """
        Create an Id128 from a single integer.
        """
        return cls(intvalue, 0)

    def as_int(self) -> int:
        """
        Interpret an Id128 as a single integer.
        """
        val: int = self.u1
        return val

    @classmethod
    def from_tuple(cls, inttuple: Tuple[int, int]) -> Id128:
        """
        Create an Id128 from a two-ints tuple.
        """
        return cls(*inttuple)

    def as_tuple(self) -> Tuple[int, int]:
        """
        Interpret an Id128 as a two-int tuple.
        """
        return (self.u1, self.u2)


# pylint: disable=invalid-name
class EventType(IntEnum):
    """
    EventTypes generated by the EBPF code.
    """

    ExecutorRun = 1
    ExecutorFinish = 2
    DropPortalEnter = 3
    DropPortalReturn = 4
    ExecProcNodeFirst = 5
    ExecEndNode = 6
    KBlockRqIssue = 7
    StackSample = 8
    MemoryResponseQueryInstr = 9
    MemoryResponseNodeInstr = 10
    MemoryNodeData = 11
    GUCResponse = 12
    MemoryAccount = 13
    ProcessFork = 14
    ProcessExit = 15


instrument_type = ct.c_byte * 0


class StubStructure(ct.Structure):
    """
    StubStructure definition, which actual fields must be updated at runtime.
    """

    _protofields: List[Tuple[str, Type[ct._CData]]] = []

    @classmethod
    def update_fields(cls, fields: Dict[str, Type[ct._CData]]) -> None:
        """
        Update the structure fields.
        """
        if hasattr(cls, "_fields_"):
            # We are not allowed to update it. But if all updated values are
            # the same as the first update, we don't care.
            fields_dict = dict(cls._fields_)  # type: ignore
            for key, value in fields.items():
                if fields_dict[key] != value:
                    raise ValueError("Cannot update a struct more than once.")
            return
        fields_dict = dict(cls._protofields)
        fields_dict.update(fields)
        cls._fields_ = list(fields_dict.items())


MAX_QUERY_LENGTH = 2048
MAX_SEARCHPATH_LENGTH = 1024


class event_base(ct.Structure):
    """
    Common fields for all events.
    """

    _fields_ = [("event_type", ct.c_short), ("pid", ct.c_int)]


class portal_data(StubStructure):
    """
    Represents the portal_data associated to a portal.
    """

    _protofields = [
        ("event", event_base),
        ("portal_key", Id128),
        ("query_addr", ct.c_ulonglong),
        ("query_id", ct.c_ulonglong),
        ("startup_cost", ct.c_double),
        ("total_cost", ct.c_double),
        ("plan_rows", ct.c_double),
        ("query", ct.c_char * MAX_QUERY_LENGTH),
        ("instrument", instrument_type),
        ("search_path", ct.c_char * MAX_SEARCHPATH_LENGTH),
    ]


class io_req_data(ct.Structure):
    """
    Represents the io_req_data coming from instrumenting the kernel.
    """

    _fields_ = [
        ("event", event_base),
        ("rwbs", ct.c_char * 8),
        ("bytes", ct.c_ulonglong),
    ]


class plan_data(ct.Structure):
    """
    Represents the data associated with a PlanNode.
    """

    _fields_ = [
        ("plan_addr", ct.c_ulonglong),
        ("plan_tag", ct.c_int),
        ("startup_cost", ct.c_double),
        ("total_cost", ct.c_double),
        ("plan_rows", ct.c_double),
        ("plan_width", ct.c_int),
        ("parallel_aware", ct.c_bool),
    ]


class planstate_data(StubStructure):
    """
    Represents the data associated to a PlanState node.
    """

    _protofields = [
        ("event", event_base),
        ("portal_key", Id128),
        ("planstate_addr", ct.c_ulonglong),
        ("planstate_tag", ct.c_int),
        ("lefttree", ct.c_ulonglong),
        ("righttree", ct.c_ulonglong),
        ("plan_data", plan_data),
        ("instrument", instrument_type),
        ("stack_capture", stack_data_t),
    ]


MEMORY_REQUEST_MAXSIZE = 131072
MEMORY_PATH_SIZE = 5


class memory_request(ct.Structure):
    """
    Represents a memory request, to be processed in the perf event handler.
    """

    _fields_ = [
        ("event_type", ct.c_short),
        ("request_id", Id128),
        ("path_size", ct.c_int),
        ("size", ct.c_ulonglong),
        ("memory_path", ct.c_ulonglong * MEMORY_PATH_SIZE),
    ]


class memory_response(ct.Structure):
    """
    Represents a memory response, sent back from the perf event handler.
    """

    _fields_ = [
        ("event", event_base),
        ("request_id", Id128),
        ("payload", ct.c_char * MEMORY_REQUEST_MAXSIZE),
    ]

    @property
    def payload_addr(self) -> int:
        """
        Returns the address of the payload field: useful to parse it into it's
        own struct.
        """
        return ct.addressof(self) + memory_response.payload.offset


class stack_sample(StubStructure):
    """
    Represents a stack sample, sent back from the perf event handler.
    """

    _protofields = [("portal_data", portal_data), ("stack_data", stack_data_t)]
