package compiler

// This file contains helper functions to create calls to LLVM intrinsics.

import (
	"strconv"

	"golang.org/x/tools/go/ssa"
	"tinygo.org/x/go-llvm"
)

// createMemoryCopyCall creates a call to a builtin LLVM memcpy or memmove
// function, declaring this function if needed. These calls are treated
// specially by optimization passes possibly resulting in better generated code,
// and will otherwise be lowered to regular libc memcpy/memmove calls.
func (b *builder) createMemoryCopyCall(fn *ssa.Function, args []ssa.Value) (llvm.Value, error) {
	fnName := "llvm." + fn.Name() + ".p0i8.p0i8.i" + strconv.Itoa(b.uintptrType.IntTypeWidth())
	llvmFn := b.mod.NamedFunction(fnName)
	if llvmFn.IsNil() {
		fnType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.i8ptrType, b.i8ptrType, b.uintptrType, b.ctx.Int1Type()}, false)
		llvmFn = llvm.AddFunction(b.mod, fnName, fnType)
	}
	var params []llvm.Value
	for _, param := range args {
		params = append(params, b.getValue(param))
	}
	params = append(params, llvm.ConstInt(b.ctx.Int1Type(), 0, false))
	b.CreateCall(llvmFn, params, "")
	return llvm.Value{}, nil
}

// createMemoryZeroCall creates calls to llvm.memset.* to zero a block of
// memory, declaring the function if needed. These calls will be lowered to
// regular libc memset calls if they aren't optimized out in a different way.
func (b *builder) createMemoryZeroCall(args []ssa.Value) (llvm.Value, error) {
	fnName := "llvm.memset.p0i8.i" + strconv.Itoa(b.uintptrType.IntTypeWidth())
	llvmFn := b.mod.NamedFunction(fnName)
	if llvmFn.IsNil() {
		fnType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.i8ptrType, b.ctx.Int8Type(), b.uintptrType, b.ctx.Int1Type()}, false)
		llvmFn = llvm.AddFunction(b.mod, fnName, fnType)
	}
	params := []llvm.Value{
		b.getValue(args[0]),
		llvm.ConstInt(b.ctx.Int8Type(), 0, false),
		b.getValue(args[1]),
		llvm.ConstInt(b.ctx.Int1Type(), 0, false),
	}
	b.CreateCall(llvmFn, params, "")
	return llvm.Value{}, nil
}

var mathToLLVMMapping = map[string]string{
	"math.Sqrt":  "llvm.sqrt.f64",
	"math.Floor": "llvm.floor.f64",
	"math.Ceil":  "llvm.ceil.f64",
	"math.Trunc": "llvm.trunc.f64",
}

// createMathOp tries to lower the given call as a LLVM math intrinsic, if
// possible. It returns the call result if possible, and a boolean whether it
// succeeded. If it doesn't succeed, the architecture doesn't support the given
// intrinsic.
func (b *builder) createMathOp(call *ssa.CallCommon) (llvm.Value, bool) {
	// Check whether this intrinsic is supported on the given GOARCH.
	// If it is unsupported, this can have two reasons:
	//
	//  1. LLVM can expand the intrinsic inline (using float instructions), but
	//     the result doesn't pass the tests of the math package.
	//  2. LLVM cannot expand the intrinsic inline, will therefore lower it as a
	//     libm function call, but the libm function call also fails the math
	//     package tests.
	//
	// Whatever the implementation, it must pass the tests in the math package
	// so unfortunately only the below intrinsic+architecture combinations are
	// supported.
	name := call.StaticCallee().RelString(nil)
	switch name {
	case "math.Ceil", "math.Floor", "math.Trunc":
		if b.GOARCH != "wasm" && b.GOARCH != "arm64" {
			return llvm.Value{}, false
		}
	case "math.Sqrt":
		if b.GOARCH != "wasm" && b.GOARCH != "amd64" && b.GOARCH != "386" {
			return llvm.Value{}, false
		}
	default:
		return llvm.Value{}, false // only the above functions are supported.
	}

	llvmFn := b.mod.NamedFunction(mathToLLVMMapping[name])
	if llvmFn.IsNil() {
		// The intrinsic doesn't exist yet, so declare it.
		// At the moment, all supported intrinsics have the form "double
		// foo(double %x)" so we can hardcode the signature here.
		llvmType := llvm.FunctionType(b.ctx.DoubleType(), []llvm.Type{b.ctx.DoubleType()}, false)
		llvmFn = llvm.AddFunction(b.mod, mathToLLVMMapping[name], llvmType)
	}
	// Create a call to the intrinsic.
	args := make([]llvm.Value, len(call.Args))
	for i, arg := range call.Args {
		args[i] = b.getValue(arg)
	}
	return b.CreateCall(llvmFn, args, ""), true
}
