// Copyright 2023 CUE Authors
//
// 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
//
//     http://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.

package wasm

import (
	"context"
	"fmt"
	"os"
	"sync"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

// A runtime is a Wasm runtime that can compile, load, and execute
// Wasm code.
type runtime struct {
	// ctx exists so that we have something to pass to Wazero
	// functions, but it's unused otherwise.
	ctx context.Context

	wazero.Runtime
}

func newRuntime() runtime {
	ctx := context.Background()
	r := wazero.NewRuntime(ctx)
	wasi_snapshot_preview1.MustInstantiate(ctx, r)

	return runtime{
		ctx:     ctx,
		Runtime: r,
	}
}

// compile takes the name of a Wasm module, and returns its compiled
// form, or an error.
func (r *runtime) compile(name string) (*module, error) {
	buf, err := os.ReadFile(name)
	if err != nil {
		return nil, fmt.Errorf("can't compile Wasm module: %w", err)
	}

	mod, err := r.Runtime.CompileModule(r.ctx, buf)
	if err != nil {
		return nil, fmt.Errorf("can't compile Wasm module: %w", err)
	}
	return &module{
		runtime:        r,
		name:           name,
		CompiledModule: mod,
	}, nil
}

// compileAndLoad is a convenience method that compiles a module then
// loads it into memory returning the loaded instance, or an error.
func (r *runtime) compileAndLoad(name string) (*instance, error) {
	m, err := r.compile(name)
	if err != nil {
		return nil, err
	}
	i, err := m.load()
	if err != nil {
		return nil, err
	}
	return i, nil
}

// A module is a compiled Wasm module.
type module struct {
	*runtime
	name string
	wazero.CompiledModule
}

// load loads the compiled module into memory, returning a new instance
// that can be called into, or an error. Different instances of the
// same module do not share memory.
func (m *module) load() (*instance, error) {
	cfg := wazero.NewModuleConfig().WithName(m.name)
	wInst, err := m.Runtime.InstantiateModule(m.ctx, m.CompiledModule, cfg)
	if err != nil {
		return nil, fmt.Errorf("can't instantiate Wasm module: %w", err)
	}

	inst := instance{
		module:   m,
		instance: wInst,
		alloc:    wInst.ExportedFunction("allocate"),
		free:     wInst.ExportedFunction("deallocate"),
	}
	return &inst, nil
}

// An instance is a Wasm module loaded into memory.
type instance struct {
	// mu serializes access the whole struct.
	mu sync.Mutex

	*module
	instance api.Module

	// alloc is a guest function that allocates guest memory on
	// behalf of the host.
	alloc api.Function

	// free is a guest function that frees guest memory on
	// behalf of the host.
	free api.Function
}

// load attempts to load the named function from the instance, returning
// it if found, or an error.
func (i *instance) load(funcName string) (api.Function, error) {
	i.mu.Lock()
	defer i.mu.Unlock()

	f := i.instance.ExportedFunction(funcName)
	if f == nil {
		return nil, fmt.Errorf("can't find function %q in Wasm module %v", funcName, i.module.Name())
	}
	return f, nil
}

// Alloc returns a reference to newly allocated guest memory that spans
// the provided size.
func (i *instance) Alloc(size uint32) (*memory, error) {
	i.mu.Lock()
	defer i.mu.Unlock()

	res, err := i.alloc.Call(i.ctx, uint64(size))
	if err != nil {
		return nil, fmt.Errorf("can't allocate memory: requested %d bytes", size)
	}
	return &memory{
		i:   i,
		ptr: uint32(res[0]),
		len: size,
	}, nil
}

// Free frees previously allocated guest memory.
func (i *instance) Free(m *memory) {
	i.mu.Lock()
	defer i.mu.Unlock()

	i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len))
}

// Free frees several previously allocated guest memories.
func (i *instance) FreeAll(ms []*memory) {
	i.mu.Lock()
	defer i.mu.Unlock()

	for _, m := range ms {
		i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len))
	}
}

// memory is a read and write reference to guest memory that the host
// requested.
type memory struct {
	i   *instance
	ptr uint32
	len uint32
}

// Bytes return a copy of the contents of the guest memory to the host.
func (m *memory) Bytes() []byte {
	m.i.mu.Lock()
	defer m.i.mu.Unlock()

	bytes, ok := m.i.instance.Memory().Read(m.ptr, m.len)
	if !ok {
		panic(fmt.Sprintf("can't read %d bytes from Wasm address %#x", m.len, m.ptr))
	}
	return append([]byte{}, bytes...)
}

// WriteAt writes p at the given relative offset within m.
// It panics if buf doesn't fit into m, or if off is out of bounds.
func (m *memory) WriteAt(p []byte, off int64) (int, error) {
	if (off < 0) || (off >= 1<<32-1) {
		panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr))
	}

	m.i.mu.Lock()
	defer m.i.mu.Unlock()

	ok := m.i.instance.Memory().Write(m.ptr+uint32(off), p)
	if !ok {
		panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr))
	}
	return len(p), nil
}

// Args returns a memory in the form of pair of arguments directly
// passable to Wasm.
func (m *memory) Args() []uint64 {
	return []uint64{uint64(m.ptr), uint64(m.len)}
}
