package bench

import (
	"context"
	"crypto/rand"
	_ "embed"
	"fmt"
	"runtime"
	"testing"

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

// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")

// caseWasm was compiled from TinyGo testdata/case.go
//
//go:embed testdata/case.wasm
var caseWasm []byte

func BenchmarkInvocation(b *testing.B) {
	b.Run("interpreter", func(b *testing.B) {
		m := instantiateHostFunctionModuleWithEngine(b, wazero.NewRuntimeConfigInterpreter())
		defer m.Close(testCtx)
		runAllInvocationBenches(b, m)
	})
	if runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" {
		b.Run("compiler", func(b *testing.B) {
			m := instantiateHostFunctionModuleWithEngine(b, wazero.NewRuntimeConfigCompiler())
			defer m.Close(testCtx)
			runAllInvocationBenches(b, m)
		})
	}
}

func BenchmarkInitialization(b *testing.B) {
	b.Run("interpreter", func(b *testing.B) {
		r := createRuntime(b, wazero.NewRuntimeConfigInterpreter())
		runInitializationBench(b, r)
	})

	b.Run("interpreter-multiple", func(b *testing.B) {
		r := createRuntime(b, wazero.NewRuntimeConfigInterpreter())
		runInitializationConcurrentBench(b, r)
	})

	if platform.CompilerSupported() {
		b.Run("compiler", func(b *testing.B) {
			r := createRuntime(b, wazero.NewRuntimeConfigCompiler())
			runInitializationBench(b, r)
		})

		b.Run("compiler-multiple", func(b *testing.B) {
			r := createRuntime(b, wazero.NewRuntimeConfigCompiler())
			runInitializationConcurrentBench(b, r)
		})
	}
}

func BenchmarkCompilation(b *testing.B) {
	if !platform.CompilerSupported() {
		b.Skip()
	}

	// Note: recreate runtime each time in the loop to ensure that
	// recompilation happens if the extern cache is not used.
	b.Run("with extern cache", func(b *testing.B) {
		cache, err := wazero.NewCompilationCacheWithDir(b.TempDir())
		if err != nil {
			b.Fatal(err)
		}
		for i := 0; i < b.N; i++ {
			r := wazero.NewRuntimeWithConfig(context.Background(), wazero.NewRuntimeConfigCompiler().WithCompilationCache(cache))
			runCompilation(b, r)
		}
	})
	b.Run("without extern cache", func(b *testing.B) {
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			r := wazero.NewRuntimeWithConfig(context.Background(), wazero.NewRuntimeConfigCompiler())
			runCompilation(b, r)
		}
	})
	b.Run("interpreter", func(b *testing.B) {
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			r := wazero.NewRuntimeWithConfig(context.Background(), wazero.NewRuntimeConfigInterpreter())
			runCompilation(b, r)
		}
	})
}

func runCompilation(b *testing.B, r wazero.Runtime) wazero.CompiledModule {
	compiled, err := r.CompileModule(testCtx, caseWasm)
	if err != nil {
		b.Fatal(err)
	}
	return compiled
}

func runInitializationBench(b *testing.B, r wazero.Runtime) {
	compiled := runCompilation(b, r)
	defer compiled.Close(testCtx)
	// Configure with real sources to avoid performance hit initializing fake ones. These sources are not used
	// in the benchmark.
	config := wazero.NewModuleConfig().WithSysNanotime().WithSysWalltime().WithRandSource(rand.Reader).
		// To measure the pure instantiation time without including calling _start.
		WithStartFunctions()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		mod, err := r.InstantiateModule(testCtx, compiled, config)
		if err != nil {
			b.Fatal(err)
		}
		mod.Close(testCtx)
	}
}

func runInitializationConcurrentBench(b *testing.B, r wazero.Runtime) {
	compiled := runCompilation(b, r)
	defer compiled.Close(testCtx)
	// Configure with real sources to avoid performance hit initializing fake ones. These sources are not used
	// in the benchmark.
	config := wazero.NewModuleConfig().
		WithSysNanotime().
		WithSysWalltime().
		WithRandSource(rand.Reader).
		// To measure the pure instantiation time without including calling _start.
		WithStartFunctions().
		WithName("")
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			m, err := r.InstantiateModule(testCtx, compiled, config)
			if err != nil {
				b.Error(err)
			} else {
				m.Close(testCtx)
			}
		}
	})
}

func runAllInvocationBenches(b *testing.B, m api.Module) {
	runBase64Benches(b, m)
	runFibBenches(b, m)
	runStringManipulationBenches(b, m)
	runReverseArrayBenches(b, m)
	runRandomMatMul(b, m)
}

func runBase64Benches(b *testing.B, m api.Module) {
	base64 := m.ExportedFunction("base64")

	for _, numPerExec := range []int{5, 100, 10000} {
		numPerExec := uint64(numPerExec)
		b.ResetTimer()
		b.Run(fmt.Sprintf("base64_%d_per_exec", numPerExec), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				if _, err := base64.Call(testCtx, numPerExec); err != nil {
					b.Fatal(err)
				}
			}
		})
	}
}

func runFibBenches(b *testing.B, m api.Module) {
	fibonacci := m.ExportedFunction("fibonacci")

	for _, num := range []int{5, 10, 20, 30} {
		num := uint64(num)
		b.ResetTimer()
		b.Run(fmt.Sprintf("fib_for_%d", num), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				if _, err := fibonacci.Call(testCtx, num); err != nil {
					b.Fatal(err)
				}
			}
		})
	}
}

func runStringManipulationBenches(b *testing.B, m api.Module) {
	stringManipulation := m.ExportedFunction("string_manipulation")

	for _, initialSize := range []int{50, 100, 1000} {
		initialSize := uint64(initialSize)
		b.ResetTimer()
		b.Run(fmt.Sprintf("string_manipulation_size_%d", initialSize), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				if _, err := stringManipulation.Call(testCtx, initialSize); err != nil {
					b.Fatal(err)
				}
			}
		})
	}
}

func runReverseArrayBenches(b *testing.B, m api.Module) {
	reverseArray := m.ExportedFunction("reverse_array")

	for _, arraySize := range []int{500, 1000, 10000} {
		arraySize := uint64(arraySize)
		b.ResetTimer()
		b.Run(fmt.Sprintf("reverse_array_size_%d", arraySize), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				if _, err := reverseArray.Call(testCtx, arraySize); err != nil {
					b.Fatal(err)
				}
			}
		})
	}
}

func runRandomMatMul(b *testing.B, m api.Module) {
	randomMatMul := m.ExportedFunction("random_mat_mul")

	for _, matrixSize := range []int{5, 10, 20} {
		matrixSize := uint64(matrixSize)
		b.ResetTimer()
		b.Run(fmt.Sprintf("random_mat_mul_size_%d", matrixSize), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				if _, err := randomMatMul.Call(testCtx, matrixSize); err != nil {
					b.Fatal(err)
				}
			}
		})
	}
}

func instantiateHostFunctionModuleWithEngine(b *testing.B, config wazero.RuntimeConfig) api.Module {
	r := createRuntime(b, config)

	// Instantiate runs the "_start" function which is what TinyGo compiles "main" to.
	m, err := r.Instantiate(testCtx, caseWasm)
	if err != nil {
		b.Fatal(err)
	}
	return m
}

func createRuntime(b *testing.B, config wazero.RuntimeConfig) wazero.Runtime {
	getRandomString := func(ctx context.Context, m api.Module, retBufPtr uint32, retBufSize uint32) {
		results, err := m.ExportedFunction("allocate_buffer").Call(ctx, 10)
		if err != nil {
			b.Fatal(err)
		}

		offset := uint32(results[0])
		m.Memory().WriteUint32Le(retBufPtr, offset)
		m.Memory().WriteUint32Le(retBufSize, 10)
		b := make([]byte, 10)
		_, _ = rand.Read(b)
		m.Memory().Write(offset, b)
	}

	r := wazero.NewRuntimeWithConfig(testCtx, config)

	_, err := r.NewHostModuleBuilder("env").
		NewFunctionBuilder().WithFunc(getRandomString).Export("get_random_string").
		Instantiate(testCtx)
	if err != nil {
		b.Fatal(err)
	}

	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
	// Add WASI to satisfy import tests
	wasi_snapshot_preview1.MustInstantiate(testCtx, r)
	return r
}
