// Package goarch isolates code from runtime.GOARCH=wasm in a way that avoids
// cyclic dependencies when re-used from other packages.
package goarch

import (
	"context"
	"encoding/binary"

	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/internal/gojs/custom"
	"github.com/tetratelabs/wazero/internal/gojs/util"
	"github.com/tetratelabs/wazero/internal/wasm"
)

// StubFunction stubs functions not used in Go's main source tree.
// This traps (unreachable opcode) to ensure the function is never called.
func StubFunction(name string) *wasm.HostFunc {
	return &wasm.HostFunc{
		ExportName: name,
		Name:       name,
		ParamTypes: []wasm.ValueType{wasm.ValueTypeI32},
		ParamNames: []string{"sp"},
		Code:       wasm.Code{GoFunc: api.GoModuleFunc(func(ctx context.Context, _ api.Module, stack []uint64) {})},
	}
}

var le = binary.LittleEndian

type Stack interface {
	// Name is the function name being invoked.
	Name() string

	Param(i int) uint64

	// ParamBytes reads a byte slice, given its memory offset and length (stack
	// positions i, i+1)
	ParamBytes(mem api.Memory, i int) []byte

	// ParamString reads a string, given its memory offset and length (stack
	// positions i, i+1)
	ParamString(mem api.Memory, i int) string

	ParamUint32(i int) uint32

	// Refresh the stack from the current stack pointer (SP).
	//
	// Note: This is needed prior to storing a value when in an operation that
	// can trigger a Go event handler.
	Refresh(api.Module)

	SetResult(i int, v uint64)

	SetResultBool(i int, v bool)

	SetResultI32(i int, v int32)

	SetResultI64(i int, v int64)

	SetResultUint32(i int, v uint32)
}

func NewStack(name string, mem api.Memory, sp uint32) Stack {
	names := custom.NameSection[name]
	s := &stack{name: name, paramCount: len(names.ParamNames), resultCount: len(names.ResultNames)}
	s.refresh(mem, sp)
	return s
}

type stack struct {
	name                    string
	paramCount, resultCount int
	buf                     []byte
}

// Name implements Stack.Name
func (s *stack) Name() string {
	return s.name
}

// Param implements Stack.Param
func (s *stack) Param(i int) (res uint64) {
	pos := i << 3
	res = le.Uint64(s.buf[pos:])
	return
}

// ParamBytes implements Stack.ParamBytes
func (s *stack) ParamBytes(mem api.Memory, i int) (res []byte) {
	offset := s.ParamUint32(i)
	byteCount := s.ParamUint32(i + 1)
	return util.MustRead(mem, s.name, i, offset, byteCount)
}

// ParamString implements Stack.ParamString
func (s *stack) ParamString(mem api.Memory, i int) string {
	return string(s.ParamBytes(mem, i)) // safe copy of guest memory
}

// ParamUint32 implements Stack.ParamUint32
func (s *stack) ParamUint32(i int) uint32 {
	return uint32(s.Param(i))
}

// Refresh implements Stack.Refresh
func (s *stack) Refresh(mod api.Module) {
	s.refresh(mod.Memory(), GetSP(mod))
}

func (s *stack) refresh(mem api.Memory, sp uint32) {
	count := uint32(s.paramCount + s.resultCount)
	buf, ok := mem.Read(sp+8, count<<3)
	if !ok {
		panic("out of memory reading stack")
	}
	s.buf = buf
}

// SetResult implements Stack.SetResult
func (s *stack) SetResult(i int, v uint64) {
	pos := (s.paramCount + i) << 3
	le.PutUint64(s.buf[pos:], v)
}

// SetResultBool implements Stack.SetResultBool
func (s *stack) SetResultBool(i int, v bool) {
	if v {
		s.SetResultUint32(i, 1)
	} else {
		s.SetResultUint32(i, 0)
	}
}

// SetResultI32 implements Stack.SetResultI32
func (s *stack) SetResultI32(i int, v int32) {
	s.SetResult(i, uint64(v))
}

// SetResultI64 implements Stack.SetResultI64
func (s *stack) SetResultI64(i int, v int64) {
	s.SetResult(i, uint64(v))
}

// SetResultUint32 implements Stack.SetResultUint32
func (s *stack) SetResultUint32(i int, v uint32) {
	s.SetResult(i, uint64(v))
}

// GetSP gets the stack pointer, which is needed prior to storing a value when
// in an operation that can trigger a Go event handler.
//
// See https://github.com/golang/go/blob/go1.20/misc/wasm/wasm_exec.js#L210-L213
func GetSP(mod api.Module) uint32 {
	// Cheat by reading global[0] directly instead of through a function proxy.
	// https://github.com/golang/go/blob/go1.20/src/runtime/rt0_js_wasm.s#L87-L90
	return uint32(mod.(*wasm.ModuleInstance).GlobalVal(0))
}

func NewFunc(name string, goFunc Func) *wasm.HostFunc {
	return util.NewFunc(name, (&stackFunc{name: name, f: goFunc}).Call)
}

type Func func(context.Context, api.Module, Stack)

type stackFunc struct {
	name string
	f    Func
}

// Call implements the same method as defined on api.GoModuleFunction.
func (f *stackFunc) Call(ctx context.Context, mod api.Module, wasmStack []uint64) {
	s := NewStack(f.name, mod.Memory(), uint32(wasmStack[0]))
	f.f(ctx, mod, s)
}
