package transform

import (
	"fmt"
	"strings"

	"tinygo.org/x/go-llvm"
)

// LowerInterrupts creates interrupt handlers for the interrupts created by
// runtime/interrupt.New.
//
// The operation is as follows. The compiler creates the following during IR
// generation:
//   - calls to runtime/interrupt.callHandlers with an interrupt number.
//   - runtime/interrupt.handle objects that store the (constant) interrupt ID and
//     interrupt handler func value.
//
// This pass then replaces those callHandlers calls with calls to the actual
// interrupt handlers. If there are no interrupt handlers for the given call,
// the interrupt handler is removed. For hardware vectoring, that means that the
// entire function is removed. For software vectoring, that means that the call
// is replaced with an 'unreachable' instruction.
// This might seem like it causes extra overhead, but in fact inlining and const
// propagation will eliminate most if not all of that.
func LowerInterrupts(mod llvm.Module) []error {
	var errs []error

	ctx := mod.Context()
	builder := ctx.NewBuilder()
	defer builder.Dispose()

	// Collect a map of interrupt handle objects. The fact that they still
	// exist in the IR indicates that they could not be optimized away,
	// therefore we need to make real interrupt handlers for them.
	handleMap := map[int64][]llvm.Value{}
	handleType := mod.GetTypeByName("runtime/interrupt.handle")
	if !handleType.IsNil() {
		for global := mod.FirstGlobal(); !global.IsNil(); global = llvm.NextGlobal(global) {
			if global.GlobalValueType() != handleType {
				continue
			}

			// Get the interrupt number from the initializer
			initializer := global.Initializer()
			interrupt := builder.CreateExtractValue(initializer, 2, "")
			num := builder.CreateExtractValue(interrupt, 0, "").SExtValue()
			pkg := packageFromInterruptHandle(global)

			handles, exists := handleMap[num]

			// If there is an existing interrupt handler, ensure it is in the same package
			// as the new one.  This is to prevent any assumptions in code that a single
			// compiler pass can see all packages to chain interrupt handlers. When packages are
			// compiled to separate object files, the linker should spot the duplicate symbols
			// for the wrapper function, failing the build.
			if exists && packageFromInterruptHandle(handles[0]) != pkg {
				errs = append(errs, errorAt(global,
					fmt.Sprintf("handlers for interrupt %d in multiple packages: %s and %s",
						num, pkg, packageFromInterruptHandle(handles[0]))))
				continue
			}

			handleMap[num] = append(handles, global)
		}
	}

	// Discover interrupts. The runtime/interrupt.callHandlers call is a
	// compiler intrinsic that is replaced with the handlers for the given
	// function.
	for _, call := range getUses(mod.NamedFunction("runtime/interrupt.callHandlers")) {
		if call.IsACallInst().IsNil() {
			errs = append(errs, errorAt(call, "expected a call to runtime/interrupt.callHandlers?"))
			continue
		}

		num := call.Operand(0)
		if num.IsAConstantInt().IsNil() {
			errs = append(errs, errorAt(call, "non-constant interrupt number?"))
			call.InstructionParent().Parent().Dump()
			continue
		}

		handlers := handleMap[num.SExtValue()]
		if len(handlers) != 0 {
			// This interrupt has at least one handler.
			// Replace the callHandlers call with (possibly multiple) calls to
			// these handlers.
			builder.SetInsertPointBefore(call)
			for _, handler := range handlers {
				initializer := handler.Initializer()
				context := builder.CreateExtractValue(initializer, 0, "")
				funcPtr := builder.CreateExtractValue(initializer, 1, "").Operand(0)
				builder.CreateCall(funcPtr.GlobalValueType(), funcPtr, []llvm.Value{
					num,
					context,
				}, "")
			}
			call.EraseFromParentAsInstruction()
		} else {
			// No handlers. Remove the call.
			fn := call.InstructionParent().Parent()
			if fn.Linkage() == llvm.ExternalLinkage {
				// Hardware vectoring. Remove the function entirely (redirecting
				// it to the default handler).
				fn.ReplaceAllUsesWith(llvm.Undef(fn.Type()))
				fn.EraseFromParentAsFunction()
			} else {
				// Software vectoring. Erase the instruction and replace it with
				// 'unreachable'.
				builder.SetInsertPointBefore(call)
				builder.CreateUnreachable()
				// Erase all instructions that follow the unreachable
				// instruction (which is a block terminator).
				inst := call
				for !inst.IsNil() {
					next := llvm.NextInstruction(inst)
					inst.EraseFromParentAsInstruction()
					inst = next
				}
			}
		}
	}

	// Replace all ptrtoint uses of the interrupt handler globals with the real
	// interrupt ID.
	// This can now be safely done after interrupts have been lowered, doing it
	// earlier may result in this interrupt handler being optimized away
	// entirely (which is not what we want).
	for num, handlers := range handleMap {
		for _, handler := range handlers {
			for _, user := range getUses(handler) {
				if user.IsAConstantExpr().IsNil() || user.Opcode() != llvm.PtrToInt {
					errs = append(errs, errorAt(handler, "internal error: expected a ptrtoint"))
					continue
				}
				user.ReplaceAllUsesWith(llvm.ConstInt(user.Type(), uint64(num), true))
			}

			// The runtime/interrupt.handle struct can finally be removed.
			// It would probably be eliminated anyway by a globaldce pass but it's
			// better to do it now to be sure.
			handler.EraseFromParentAsGlobal()
		}
	}

	// Remove now-useless runtime/interrupt.use calls. These are used for some
	// platforms like AVR that do not need to enable interrupts to use them, so
	// need another way to keep them alive.
	// After interrupts have been lowered, this call is useless and would cause
	// a linker error so must be removed.
	for _, call := range getUses(mod.NamedFunction("runtime/interrupt.use")) {
		if call.IsACallInst().IsNil() {
			errs = append(errs, errorAt(call, "internal error: expected call to runtime/interrupt.use"))
			continue
		}

		call.EraseFromParentAsInstruction()
	}

	return errs
}

func packageFromInterruptHandle(handle llvm.Value) string {
	return strings.Split(handle.Name(), "$")[0]
}
