package compiler

// This file implements inline asm support by calling special functions.

import (
	"fmt"
	"go/constant"
	"go/token"
	"regexp"
	"strconv"
	"strings"

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

// This is a compiler builtin, which emits a piece of inline assembly with no
// operands or return values. It is useful for trivial instructions, like wfi in
// ARM or sleep in AVR.
//
//	func Asm(asm string)
//
// The provided assembly must be a constant.
func (b *builder) createInlineAsm(args []ssa.Value) (llvm.Value, error) {
	// Magic function: insert inline assembly instead of calling it.
	fnType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{}, false)
	asm := constant.StringVal(args[0].(*ssa.Const).Value)
	target := llvm.InlineAsm(fnType, asm, "", true, false, 0, false)
	return b.CreateCall(fnType, target, nil, ""), nil
}

// This is a compiler builtin, which allows assembly to be called in a flexible
// way.
//
//	func AsmFull(asm string, regs map[string]interface{}) uintptr
//
// The asm parameter must be a constant string. The regs parameter must be
// provided immediately. For example:
//
//	arm.AsmFull(
//	    "str {value}, [{result}]",
//	    map[string]interface{}{
//	        "value":  1,
//	        "result": uintptr(unsafe.Pointer(&dest)),
//	    })
func (b *builder) createInlineAsmFull(instr *ssa.CallCommon) (llvm.Value, error) {
	asmString := constant.StringVal(instr.Args[0].(*ssa.Const).Value)
	registers := map[string]llvm.Value{}
	if registerMap, ok := instr.Args[1].(*ssa.MakeMap); ok {
		for _, r := range *registerMap.Referrers() {
			switch r := r.(type) {
			case *ssa.DebugRef:
				// ignore
			case *ssa.MapUpdate:
				if r.Block() != registerMap.Block() {
					return llvm.Value{}, b.makeError(instr.Pos(), "register value map must be created in the same basic block")
				}
				key := constant.StringVal(r.Key.(*ssa.Const).Value)
				registers[key] = b.getValue(r.Value.(*ssa.MakeInterface).X, getPos(instr))
			case *ssa.Call:
				if r.Common() == instr {
					break
				}
			default:
				return llvm.Value{}, b.makeError(instr.Pos(), "don't know how to handle argument to inline assembly: "+r.String())
			}
		}
	}
	// TODO: handle dollar signs in asm string
	registerNumbers := map[string]int{}
	var err error
	argTypes := []llvm.Type{}
	args := []llvm.Value{}
	constraints := []string{}
	hasOutput := false
	asmString = regexp.MustCompile(`\{\}`).ReplaceAllStringFunc(asmString, func(s string) string {
		hasOutput = true
		return "$0"
	})
	if hasOutput {
		constraints = append(constraints, "=&r")
		registerNumbers[""] = 0
	}
	asmString = regexp.MustCompile(`\{[a-zA-Z]+\}`).ReplaceAllStringFunc(asmString, func(s string) string {
		// TODO: skip strings like {r4} etc. that look like ARM push/pop
		// instructions.
		name := s[1 : len(s)-1]
		if _, ok := registers[name]; !ok {
			if err == nil {
				err = b.makeError(instr.Pos(), "unknown register name: "+name)
			}
			return s
		}
		if _, ok := registerNumbers[name]; !ok {
			registerNumbers[name] = len(registerNumbers)
			argTypes = append(argTypes, registers[name].Type())
			args = append(args, registers[name])
			switch registers[name].Type().TypeKind() {
			case llvm.IntegerTypeKind:
				constraints = append(constraints, "r")
			case llvm.PointerTypeKind:
				// Memory references require a type starting with LLVM 14,
				// probably as a preparation for opaque pointers.
				err = b.makeError(instr.Pos(), "support for pointer operands was dropped in TinyGo 0.23")
				return s
			default:
				err = b.makeError(instr.Pos(), "unknown type in inline assembly for value: "+name)
				return s
			}
		}
		return fmt.Sprintf("${%v}", registerNumbers[name])
	})
	if err != nil {
		return llvm.Value{}, err
	}
	var outputType llvm.Type
	if hasOutput {
		outputType = b.uintptrType
	} else {
		outputType = b.ctx.VoidType()
	}
	fnType := llvm.FunctionType(outputType, argTypes, false)
	target := llvm.InlineAsm(fnType, asmString, strings.Join(constraints, ","), true, false, 0, false)
	result := b.CreateCall(fnType, target, args, "")
	if hasOutput {
		return result, nil
	} else {
		// Make sure we return something valid.
		return llvm.ConstInt(b.uintptrType, 0, false), nil
	}
}

// This is a compiler builtin which emits an inline SVCall instruction. It can
// be one of:
//
//	func SVCall0(num uintptr) uintptr
//	func SVCall1(num uintptr, a1 interface{}) uintptr
//	func SVCall2(num uintptr, a1, a2 interface{}) uintptr
//	func SVCall3(num uintptr, a1, a2, a3 interface{}) uintptr
//	func SVCall4(num uintptr, a1, a2, a3, a4 interface{}) uintptr
//
// The num parameter must be a constant. All other parameters may be any scalar
// value supported by LLVM inline assembly.
func (b *builder) emitSVCall(args []ssa.Value, pos token.Pos) (llvm.Value, error) {
	num, _ := constant.Uint64Val(args[0].(*ssa.Const).Value)
	llvmArgs := []llvm.Value{}
	argTypes := []llvm.Type{}
	asm := "svc #" + strconv.FormatUint(num, 10)
	constraints := "={r0}"
	for i, arg := range args[1:] {
		arg = arg.(*ssa.MakeInterface).X
		if i == 0 {
			constraints += ",0"
		} else {
			constraints += ",{r" + strconv.Itoa(i) + "}"
		}
		llvmValue := b.getValue(arg, pos)
		llvmArgs = append(llvmArgs, llvmValue)
		argTypes = append(argTypes, llvmValue.Type())
	}
	// Implement the ARM calling convention by marking r1-r3 as
	// clobbered. r0 is used as an output register so doesn't have to be
	// marked as clobbered.
	constraints += ",~{r1},~{r2},~{r3}"
	fnType := llvm.FunctionType(b.uintptrType, argTypes, false)
	target := llvm.InlineAsm(fnType, asm, constraints, true, false, 0, false)
	return b.CreateCall(fnType, target, llvmArgs, ""), nil
}

// This is a compiler builtin which emits an inline SVCall instruction. It can
// be one of:
//
//	func SVCall0(num uintptr) uintptr
//	func SVCall1(num uintptr, a1 interface{}) uintptr
//	func SVCall2(num uintptr, a1, a2 interface{}) uintptr
//	func SVCall3(num uintptr, a1, a2, a3 interface{}) uintptr
//	func SVCall4(num uintptr, a1, a2, a3, a4 interface{}) uintptr
//
// The num parameter must be a constant. All other parameters may be any scalar
// value supported by LLVM inline assembly.
// Same as emitSVCall but for AArch64
func (b *builder) emitSV64Call(args []ssa.Value, pos token.Pos) (llvm.Value, error) {
	num, _ := constant.Uint64Val(args[0].(*ssa.Const).Value)
	llvmArgs := []llvm.Value{}
	argTypes := []llvm.Type{}
	asm := "svc #" + strconv.FormatUint(num, 10)
	constraints := "={x0}"
	for i, arg := range args[1:] {
		arg = arg.(*ssa.MakeInterface).X
		if i == 0 {
			constraints += ",0"
		} else {
			constraints += ",{x" + strconv.Itoa(i) + "}"
		}
		llvmValue := b.getValue(arg, pos)
		llvmArgs = append(llvmArgs, llvmValue)
		argTypes = append(argTypes, llvmValue.Type())
	}
	// Implement the ARM64 calling convention by marking x1-x7 as
	// clobbered. x0 is used as an output register so doesn't have to be
	// marked as clobbered.
	constraints += ",~{x1},~{x2},~{x3},~{x4},~{x5},~{x6},~{x7}"
	fnType := llvm.FunctionType(b.uintptrType, argTypes, false)
	target := llvm.InlineAsm(fnType, asm, constraints, true, false, 0, false)
	return b.CreateCall(fnType, target, llvmArgs, ""), nil
}

// This is a compiler builtin which emits CSR instructions. It can be one of:
//
//	func (csr CSR) Get() uintptr
//	func (csr CSR) Set(uintptr)
//	func (csr CSR) SetBits(uintptr) uintptr
//	func (csr CSR) ClearBits(uintptr) uintptr
//
// The csr parameter (method receiver) must be a constant. Other parameter can
// be any value.
func (b *builder) emitCSROperation(call *ssa.CallCommon) (llvm.Value, error) {
	csrConst, ok := call.Args[0].(*ssa.Const)
	if !ok {
		return llvm.Value{}, b.makeError(call.Pos(), "CSR must be constant")
	}
	csr := csrConst.Uint64()
	switch name := call.StaticCallee().Name(); name {
	case "Get":
		// Note that this instruction may have side effects, and thus must be
		// marked as such.
		fnType := llvm.FunctionType(b.uintptrType, nil, false)
		asm := fmt.Sprintf("csrr $0, %d", csr)
		target := llvm.InlineAsm(fnType, asm, "=r", true, false, 0, false)
		return b.CreateCall(fnType, target, nil, ""), nil
	case "Set":
		fnType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.uintptrType}, false)
		asm := fmt.Sprintf("csrw %d, $0", csr)
		target := llvm.InlineAsm(fnType, asm, "r", true, false, 0, false)
		return b.CreateCall(fnType, target, []llvm.Value{b.getValue(call.Args[1], getPos(call))}, ""), nil
	case "SetBits":
		// Note: it may be possible to optimize this to csrrsi in many cases.
		fnType := llvm.FunctionType(b.uintptrType, []llvm.Type{b.uintptrType}, false)
		asm := fmt.Sprintf("csrrs $0, %d, $1", csr)
		target := llvm.InlineAsm(fnType, asm, "=r,r", true, false, 0, false)
		return b.CreateCall(fnType, target, []llvm.Value{b.getValue(call.Args[1], getPos(call))}, ""), nil
	case "ClearBits":
		// Note: it may be possible to optimize this to csrrci in many cases.
		fnType := llvm.FunctionType(b.uintptrType, []llvm.Type{b.uintptrType}, false)
		asm := fmt.Sprintf("csrrc $0, %d, $1", csr)
		target := llvm.InlineAsm(fnType, asm, "=r,r", true, false, 0, false)
		return b.CreateCall(fnType, target, []llvm.Value{b.getValue(call.Args[1], getPos(call))}, ""), nil
	default:
		return llvm.Value{}, b.makeError(call.Pos(), "unknown CSR operation: "+name)
	}
}

// Implement runtime/interrupt.Checkpoint.Save. It needs to be implemented
// directly at the call site. If it isn't implemented directly at the call site
// (but instead through a function call), it might result in an overwritten
// stack in the non-jump return case.
func (b *builder) createInterruptCheckpoint(ptr ssa.Value) llvm.Value {
	addr := b.getValue(ptr, ptr.Pos())
	b.createNilCheck(ptr, addr, "deref")
	stackPointer := b.readStackPointer()
	b.CreateStore(stackPointer, addr)
	return b.createCheckpoint(addr)
}
