;=============================================================================
; event_state.asm - context save / restore / switch
;
; Context layout — MUST stay in sync with event_manager.h struct event_card:
;
;   ecx = evt + 84  (evt->context is at byte offset 84 from event_card start)
;
;   event_card layout:
;     event_index_tag  (aisle +0,  4 bytes)
;     name[64]         (char  +4,  64 bytes)
;     program_ptr      (ptr   +68, 4 bytes)
;     state            (enum  +72, 4 bytes)
;     actions          (ptr   +76, 4 bytes)
;     action_count     (aisle +80, 4 bytes)
;     context               at +84
;
;   [ecx +  0]  eip     (aisle = 4 bytes)
;   [ecx +  4]  esp     (aisle = 4 bytes)
;   [ecx +  8]  ebp     (aisle = 4 bytes)
;   [ecx + 12]  eax     (aisle = 4 bytes)
;   [ecx + 16]  ebx     (aisle = 4 bytes)
;   [ecx + 20]  ecx     (aisle = 4 bytes)
;   [ecx + 24]  edx     (aisle = 4 bytes)
;   [ecx + 28]  esi     (aisle = 4 bytes)
;   [ecx + 32]  edi     (aisle = 4 bytes)
;   [ecx + 36]  eflags  (aisle = 4 bytes)
;   [ecx + 40]  cs      (den   = 1 byte)
;   [ecx + 41]  ds      (den   = 1 byte)
;   [ecx + 42]  es      (den   = 1 byte)
;   [ecx + 43]  fs      (den   = 1 byte)
;   [ecx + 44]  gs      (den   = 1 byte)
;   [ecx + 45]  ss      (den   = 1 byte)
;=============================================================================
[BITS 32]

global _save_event_state
global _restore_event_state
global _switch_event_state

extern write_element
extern tss          ; from setup_isr.asm — used to update TSS.ESP0 before ring-3 iret

; Offset from start of event_card to context struct
%define CTX_BASE            84

; Offset from start of event_card to core_stack_top field
; layout after context (CTX_BASE=84, context=46 bytes + 2 pad = 48):
;   +132  stack_start       (aisle, 4)
;   +136  stack_size        (aisle, 4)
;   +140  core_stack_top  (aisle, 4)  <-- TSS.ESP0 for this ring-3 event
; MUST stay in sync with event_card in event_manager.h
%define EVT_CORE_STACK      140

; Offsets within context struct
%define C_EIP       0
%define C_ESP       4
%define C_EBP       8
%define C_EAX       12
%define C_EBX       16
%define C_ECX       20
%define C_EDX       24
%define C_ESI       28
%define C_EDI       32
%define C_EFLAGS    36
%define C_CS        40          ; den: 1 byte
%define C_DS        41          ; den: 1 byte
%define C_ES        42          ; den: 1 byte
%define C_FS        43          ; den: 1 byte
%define C_GS        44          ; den: 1 byte
%define C_SS        45          ; den: 1 byte

; Valid ring-0 and ring-3 CS selectors
%define SEL_CORE_CS 0x08
%define SEL_USER_CS 0x1B
%define SEL_CORE_DS 0x10
%define SEL_USER_DS 0x23

section .text

;=============================================================================
; _save_event_state(event *evt)
; Captures the current CPU register state into evt->context.
; Returns normally — caller continues after the call.
; Used before yielding, sleeping, or blocking so the event can be resumed.
;
; FIX: eflags is always saved with IF=1 forced on.
;
; _save_event_state is called from within IRQ handlers and syscall handlers,
; both of which run with IF=0 (interrupt gate cleared it on entry).  If we
; stored IF=0 into evt->context.eflags, then when _restore_event_state later
; pushes those flags into an iret frame and fires iret, the resumed event
; would run with interrupts permanently disabled.
;
; Any event that is saved mid-IRQ and then restored expects to continue
; normal execution — it must have interrupts on.  Forcing IF=1 here makes
; that guarantee unconditional, regardless of when save is called.
;=============================================================================
_save_event_state:
    push ebp
    mov  ebp, esp

    mov  eax, [ebp + 8]         ; eax = event ptr
    test eax, eax
    jz   .done

    lea  ecx, [eax + CTX_BASE]  ; ecx -> evt->context

    ; general purpose registers.
    ; eax held the event ptr — the caller's real eax is lost, zero it.
    ; the resumed event gets EAX = return value from the scheduler.
    mov  dword [ecx + C_EAX], 0
    mov  [ecx + C_EBX], ebx
    mov  [ecx + C_ECX], ecx     ; context pointer itself (best we can do)
    mov  [ecx + C_EDX], edx
    mov  [ecx + C_ESI], esi
    mov  [ecx + C_EDI], edi
    mov  edx, [ebp]             ; caller's real ebp (pushed by our prologue)
    mov  [ecx + C_EBP], edx

    ; ESP: where the caller's stack was before the call instruction.
    ;   current ebp + 4 (saved ebp) + 4 (return addr) + 4 (arg) = ebp + 8
    lea  edx, [ebp + 8]
    mov  [ecx + C_ESP], edx

    ; EIP: return address — where the event resumes when restored.
    mov  edx, [ebp + 4]
    mov  [ecx + C_EIP], edx

    ; EFLAGS — always force IF=1.
    ;
    ; this function is called from IRQ handlers (timer, keyboard, mouse) and
    ; from the syscall handler (int 0x80), all of which use interrupt gates
    ; and therefore run with IF=0.  storing IF=0 here means the restored
    ; event resumes with interrupts disabled — no timer, no mouse, total freeze.
    ;
    ; forcing IF=1 ensures every event resumes with interrupts on regardless
    ; of the context in which the save occurred.
    pushfd
    pop  edx
    or   edx, 0x200             ; force IF=1
    mov  [ecx + C_EFLAGS], edx

    ; segment registers (1-byte stores into den fields)
    mov  ax, cs
    mov  byte [ecx + C_CS], al
    mov  ax, ds
    mov  byte [ecx + C_DS], al
    mov  ax, es
    mov  byte [ecx + C_ES], al
    mov  ax, fs
    mov  byte [ecx + C_FS], al
    mov  ax, gs
    mov  byte [ecx + C_GS], al
    mov  ax, ss
    mov  byte [ecx + C_SS], al

.done:
    pop  ebp
    ret

;=============================================================================
; _restore_event_state(event *evt)    — NEVER RETURNS
; Fully restores CPU state from evt->context and executes iret.
; Handles ring 0 (CS=0x08) and ring 3 (CS=0x1B) iret frames correctly.
;=============================================================================
_restore_event_state:
    cli

    mov  eax, [esp + 4]         ; eax = event ptr
    test eax, eax
    jz   .error_null

    lea  ecx, [eax + CTX_BASE]  ; ecx -> evt->context

    ; check CS to determine privilege level
    movzx edx, byte [ecx + C_CS]
    cmp  edx, SEL_CORE_CS
    je   .ring0_restore
    cmp  edx, SEL_USER_CS
    je   .ring3_restore
    jmp  .error_seg

;-----------------------------------------------------------------------------
; Ring 3 restore
; iret frame (pushed in order, popped LIFO): EIP, CS, EFLAGS, ESP, SS
;-----------------------------------------------------------------------------
.ring3_restore:
    ; update TSS.ESP0 to this event's private core stack BEFORE iret.
    ; when any hardware IRQ fires while we are in ring 3, the CPU reads
    ; TSS.ESP0 to find the core stack to switch to.  without this update
    ; it uses start_stack from the linker (stale after boot) which causes
    ; immediate stack corruption -> double fault -> triple fault -> GDT error.
    mov  edx, [eax + EVT_CORE_STACK]
    test edx, edx
    jz   .ring3_build_frame             ; 0 = not a ring-3 event, skip
    mov  dword [tss + 4], edx           ; tss.esp0 = evt->core_stack_top
.ring3_build_frame:
    ; eax still = event_card*, ecx still = context ptr (eax + CTX_BASE)
    ; push iret frame onto current core stack
    movzx eax, byte [ecx + C_SS]
    push eax                            ; SS  (0x23)
    push dword [ecx + C_ESP]            ; ESP (user stack)
    push dword [ecx + C_EFLAGS]        ; EFLAGS (IF=1 — guaranteed by _save_event_state)
    movzx eax, byte [ecx + C_CS]
    push eax                            ; CS  (0x1B)
    push dword [ecx + C_EIP]           ; EIP

    ; load user data segments before iret
    movzx eax, byte [ecx + C_DS]
    mov  ds, ax
    mov  es, ax
    movzx eax, byte [ecx + C_FS]
    mov  fs, ax
    movzx eax, byte [ecx + C_GS]
    mov  gs, ax

    ; restore GPRs (ecx must be last, we are using it as pointer)
    mov  eax, [ecx + C_EAX]
    mov  ebx, [ecx + C_EBX]
    mov  edx, [ecx + C_EDX]
    mov  esi, [ecx + C_ESI]
    mov  edi, [ecx + C_EDI]
    mov  ebp, [ecx + C_EBP]
    mov  ecx, [ecx + C_ECX]

    iret

;-----------------------------------------------------------------------------
; Ring 0 restore
; iret frame (same privilege): EIP, CS, EFLAGS  (no SS/ESP push)
; We switch SS and ESP manually before iret.
;-----------------------------------------------------------------------------
.ring0_restore:
    ; switch to the saved core stack
    movzx eax, byte [ecx + C_SS]
    mov  ss, ax
    mov  esp, [ecx + C_ESP]

    ; push iret frame on the restored stack
    push dword [ecx + C_EFLAGS]        ; EFLAGS (IF=1 — guaranteed by _save_event_state)
    movzx eax, byte [ecx + C_CS]
    push eax                            ; CS  (0x08)
    push dword [ecx + C_EIP]           ; EIP

    ; segment registers
    movzx eax, byte [ecx + C_DS]
    mov  ds, ax
    mov  es, ax
    movzx eax, byte [ecx + C_FS]
    mov  fs, ax
    movzx eax, byte [ecx + C_GS]
    mov  gs, ax

    ; restore GPRs
    mov  eax, [ecx + C_EAX]
    mov  ebx, [ecx + C_EBX]
    mov  edx, [ecx + C_EDX]
    mov  esi, [ecx + C_ESI]
    mov  edi, [ecx + C_EDI]
    mov  ebp, [ecx + C_EBP]
    mov  ecx, [ecx + C_ECX]

    iret

;-----------------------------------------------------------------------------
.error_null:
    push msg_null
    call write_element
    add  esp, 4
    hlt
    jmp  $

.error_seg:
    push msg_seg
    call write_element
    add  esp, 4
    hlt
    jmp  $

;=============================================================================
; _switch_event_state(event *from, event *to)    — NEVER RETURNS
; 1. Saves current CPU context into from->context
; 2. Restores CPU context from to->context via iret
;
; Called from _yield_event() / _block_event() in event_manager.c
;=============================================================================
_switch_event_state:
    ; [esp+4] = from,  [esp+8] = to
    push dword [esp + 4]        ; arg: from
    call _save_event_state
    add  esp, 4

    ; stack is back to entry layout so [esp+8] is still to
    push dword [esp + 8]
    call _restore_event_state
    hlt                         ; unreachable

section .rodata
msg_null:   db "[EVENT_STATE] restore: NULL event ptr", 10, 0
msg_seg:    db "[EVENT_STATE] restore: invalid CS - expected 0x08 or 0x1B", 10, 0

section .note.GNU-stack noalloc noexec nowrite progbits