{% skip_file unless flag?(:x86_64) && flag?(:win32) %}

class Fiber
  # :nodoc:
  def makecontext(stack_ptr, fiber_main) : Nil
    # A great explanation on stack contexts for win32:
    # https://web.archive.org/web/20220527113808/https://cfsamson.gitbook.io/green-threads-explained-in-200-lines-of-rust/supporting-windows

    # 4 shadow space + (8 registers + 3 qwords for NT_TIB + 1 parameter) + 10 128bit XMM registers
    @context.stack_top = (stack_ptr - (4 + 12 + 10*2)).as(Void*)
    @context.resumable = 1

    # actual stack top, not including guard pages and reserved pages
    LibC.GetNativeSystemInfo(out system_info)
    stack_top = @stack.bottom - system_info.dwPageSize

    stack_ptr -= 4                    # shadow space (or home space) before return address
    stack_ptr[0] = fiber_main.pointer # %rbx: Initial `resume` will `ret` to this address
    stack_ptr[-1] = self.as(Void*)    # %rcx: puts `self` as first argument for `fiber_main`

    # The following three values are stored in the Thread Information Block (NT_TIB)
    # and are used by Windows to track the current stack limits
    stack_ptr[-2] = @stack.pointer # %gs:0x1478: Win32 DeallocationStack
    stack_ptr[-3] = stack_top      # %gs:0x10: Stack Limit
    stack_ptr[-4] = @stack.bottom  # %gs:0x08: Stack Base
  end

  # :nodoc:
  @[NoInline]
  @[Naked]
  def self.swapcontext(current_context, new_context) : Nil
    {% if compare_versions(Crystal::LLVM_VERSION, "9.0.0") >= 0 %}
      #                %rcx           , %rdx
      asm("
          pushq %rcx
          pushq %gs:0x1478  // Thread Information Block: Win32 DeallocationStack
          pushq %gs:0x10    // Thread Information Block: Stack Limit
          pushq %gs:0x08    // Thread Information Block: Stack Base
          pushq %rdi        // push 1st argument (because of initial resume)
          pushq %rbx        // push callee-saved registers on the stack
          pushq %rbp
          pushq %rsi
          pushq %r12
          pushq %r13
          pushq %r14
          pushq %r15
          subq $$160, %rsp  // push XMM registers
          movups %xmm6, 0x00(%rsp)
          movups %xmm7, 0x10(%rsp)
          movups %xmm8, 0x20(%rsp)
          movups %xmm9, 0x30(%rsp)
          movups %xmm10, 0x40(%rsp)
          movups %xmm11, 0x50(%rsp)
          movups %xmm12, 0x60(%rsp)
          movups %xmm13, 0x70(%rsp)
          movups %xmm14, 0x80(%rsp)
          movups %xmm15, 0x90(%rsp)
          movq %rsp, 0(%rcx)  // current_context.stack_top = %rsp
          movl $$1, 8(%rcx)   // current_context.resumable = 1
          movl $$0, 8(%rdx)   // new_context.resumable = 0
          movq 0(%rdx), %rsp  // %rsp = new_context.stack_top
          movups 0x00(%rsp), %xmm6 // pop XMM registers
          movups 0x10(%rsp), %xmm7
          movups 0x20(%rsp), %xmm8
          movups 0x30(%rsp), %xmm9
          movups 0x40(%rsp), %xmm10
          movups 0x50(%rsp), %xmm11
          movups 0x60(%rsp), %xmm12
          movups 0x70(%rsp), %xmm13
          movups 0x80(%rsp), %xmm14
          movups 0x90(%rsp), %xmm15
          addq $$160, %rsp
          popq %r15         // pop callee-saved registers from the stack
          popq %r14
          popq %r13
          popq %r12
          popq %rsi
          popq %rbp
          popq %rbx
          popq %rdi         // pop 1st argument (for initial resume)
          popq %gs:0x08
          popq %gs:0x10
          popq %gs:0x1478
          popq %rcx
          ")
    {% else %}
      # On LLVM < 9.0 using the previous code emits some additional
      # instructions that breaks the context switching.
      asm("
          pushq %rcx
          pushq %gs:0x1478  // Thread Information Block: Win32 DeallocationStack
          pushq %gs:0x10    // Thread Information Block: Stack Limit
          pushq %gs:0x08    // Thread Information Block: Stack Base
          pushq %rdi        // push 1st argument (because of initial resume)
          pushq %rbx        // push callee-saved registers on the stack
          pushq %rbp
          pushq %rsi
          pushq %r12
          pushq %r13
          pushq %r14
          pushq %r15
          subq $$160, %rsp  // push XMM registers
          movups %xmm6, 0x00(%rsp)
          movups %xmm7, 0x10(%rsp)
          movups %xmm8, 0x20(%rsp)
          movups %xmm9, 0x30(%rsp)
          movups %xmm10, 0x40(%rsp)
          movups %xmm11, 0x50(%rsp)
          movups %xmm12, 0x60(%rsp)
          movups %xmm13, 0x70(%rsp)
          movups %xmm14, 0x80(%rsp)
          movups %xmm15, 0x90(%rsp)
          movq %rsp, 0($0)  // current_context.stack_top = %rsp
          movl $$1, 8($0)   // current_context.resumable = 1
          movl $$0, 8($1)   // new_context.resumable = 0
          movq 0($1), %rsp  // %rsp = new_context.stack_top
          movups 0x00(%rsp), %xmm6 // pop XMM registers
          movups 0x10(%rsp), %xmm7
          movups 0x20(%rsp), %xmm8
          movups 0x30(%rsp), %xmm9
          movups 0x40(%rsp), %xmm10
          movups 0x50(%rsp), %xmm11
          movups 0x60(%rsp), %xmm12
          movups 0x70(%rsp), %xmm13
          movups 0x80(%rsp), %xmm14
          movups 0x90(%rsp), %xmm15
          addq $$160, %rsp
          popq %r15         // pop callee-saved registers from the stack
          popq %r14
          popq %r13
          popq %r12
          popq %rsi
          popq %rbp
          popq %rbx
          popq %rdi         // pop 1st argument (for initial resume)
          popq %gs:0x08
          popq %gs:0x10
          popq %gs:0x1478
          popq %rcx
          " :: "r"(current_context), "r"(new_context))
    {% end %}
  end
end
