package transform

// This file implements several small optimizations of runtime and reflect
// calls.

import (
	"strings"

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

// OptimizeStringToBytes transforms runtime.stringToBytes(...) calls into const
// []byte slices whenever possible. This optimizes the following pattern:
//
//	w.Write([]byte("foo"))
//
// where Write does not store to the slice.
func OptimizeStringToBytes(mod llvm.Module) {
	stringToBytes := mod.NamedFunction("runtime.stringToBytes")
	if stringToBytes.IsNil() {
		// nothing to optimize
		return
	}

	for _, call := range getUses(stringToBytes) {
		strptr := call.Operand(0)
		strlen := call.Operand(1)

		// strptr is always constant because strings are always constant.

		var pointerUses []llvm.Value
		canConvertPointer := true
		for _, use := range getUses(call) {
			if use.IsAExtractValueInst().IsNil() {
				// Expected an extractvalue, but this is something else.
				canConvertPointer = false
				continue
			}
			switch use.Type().TypeKind() {
			case llvm.IntegerTypeKind:
				// A length (len or cap). Propagate the length value.
				// This can always be done because the byte slice is always the
				// same length as the original string.
				use.ReplaceAllUsesWith(strlen)
				use.EraseFromParentAsInstruction()
			case llvm.PointerTypeKind:
				// The string pointer itself.
				if !isReadOnly(use) {
					// There is a store to the byte slice. This means that none
					// of the pointer uses can't be propagated.
					canConvertPointer = false
					continue
				}
				// It may be that the pointer value can be propagated, if all of
				// the pointer uses are readonly.
				pointerUses = append(pointerUses, use)
			default:
				// should not happen
				panic("unknown return type of runtime.stringToBytes: " + use.Type().String())
			}
		}
		if canConvertPointer {
			// All pointer uses are readonly, so they can be converted.
			for _, use := range pointerUses {
				use.ReplaceAllUsesWith(strptr)
				use.EraseFromParentAsInstruction()
			}

			// Call to runtime.stringToBytes can be eliminated: both the input
			// and the output is constant.
			call.EraseFromParentAsInstruction()
		}
	}
}

// OptimizeStringEqual transforms runtime.stringEqual(...) calls into simple
// integer comparisons if at least one of the sides of the comparison is zero.
// Ths converts str == "" into len(str) == 0 and "" == "" into false.
func OptimizeStringEqual(mod llvm.Module) {
	stringEqual := mod.NamedFunction("runtime.stringEqual")
	if stringEqual.IsNil() {
		// nothing to optimize
		return
	}

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

	for _, call := range getUses(stringEqual) {
		str1len := call.Operand(1)
		str2len := call.Operand(3)

		zero := llvm.ConstInt(str1len.Type(), 0, false)
		if str1len == zero || str2len == zero {
			builder.SetInsertPointBefore(call)
			icmp := builder.CreateICmp(llvm.IntEQ, str1len, str2len, "")
			call.ReplaceAllUsesWith(icmp)
			call.EraseFromParentAsInstruction()
			continue
		}
	}
}

// OptimizeReflectImplements optimizes the following code:
//
//	implements := someType.Implements(someInterfaceType)
//
// where someType is an arbitrary reflect.Type and someInterfaceType is a
// reflect.Type of interface kind, to the following code:
//
//	_, implements := someType.(interfaceType)
//
// if the interface type is known at compile time (that is, someInterfaceType is
// a LLVM constant aggregate). This optimization is especially important for the
// encoding/json package, which uses this method.
//
// As of this writing, the (reflect.Type).Interface method has not yet been
// implemented so this optimization is critical for the encoding/json package.
func OptimizeReflectImplements(mod llvm.Module) {
	implementsSignature := mod.NamedGlobal("reflect/methods.Implements(reflect.Type) bool")
	if implementsSignature.IsNil() {
		return
	}

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

	// Look up the (reflect.Value).Implements() method.
	var implementsFunc llvm.Value
	for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
		attr := fn.GetStringAttributeAtIndex(-1, "tinygo-invoke")
		if attr.IsNil() {
			continue
		}
		if attr.GetStringValue() == "reflect/methods.Implements(reflect.Type) bool" {
			implementsFunc = fn
			break
		}
	}
	if implementsFunc.IsNil() {
		// Doesn't exist in the program, so nothing to do.
		return
	}

	for _, call := range getUses(implementsFunc) {
		if call.IsACallInst().IsNil() {
			continue
		}
		interfaceType := stripPointerCasts(call.Operand(2))
		if interfaceType.IsAGlobalVariable().IsNil() {
			// Interface is unknown at compile time. This can't be optimized.
			continue
		}

		if strings.HasPrefix(interfaceType.Name(), "reflect/types.type:named:") {
			// Get the underlying type.
			interfaceType = stripPointerCasts(builder.CreateExtractValue(interfaceType.Initializer(), 3, ""))
		}
		if !strings.HasPrefix(interfaceType.Name(), "reflect/types.type:interface:") {
			// This is an error. The Type passed to Implements should be of
			// interface type. Ignore it here (don't report it), it will be
			// reported at runtime.
			continue
		}
		typeAssertFunction := mod.NamedFunction(strings.TrimPrefix(interfaceType.Name(), "reflect/types.type:") + ".$typeassert")
		if typeAssertFunction.IsNil() {
			continue
		}

		// Replace Implements call with the type assert call.
		builder.SetInsertPointBefore(call)
		implements := builder.CreateCall(typeAssertFunction.GlobalValueType(), typeAssertFunction, []llvm.Value{
			call.Operand(0), // typecode to check
		}, "")
		call.ReplaceAllUsesWith(implements)
		call.EraseFromParentAsInstruction()
	}
}
