package compiler

import (
	"github.com/tetratelabs/wazero/internal/asm"
	"github.com/tetratelabs/wazero/internal/wazeroir"
)

// compileDropRange adds instruction to drop the values on the target range
// in an architecture independent way.
func compileDropRange(c compiler, raw uint64) (err error) {
	r := wazeroir.InclusiveRangeFromU64(raw)
	locationStack := c.runtimeValueLocationStack()
	if r.Start < 0 {
		return
	} else if r.Start == 0 {
		for i := 0; i <= int(r.End); i++ {
			if loc := locationStack.pop(); loc.onRegister() {
				locationStack.releaseRegister(loc)
			}
		}
		return
	}

	// If the top value is alive, we must ensure that it is not located as a conditional.
	// Otherwise, the conditional flag might end up modified by the following operation.
	if err = c.maybeCompileMoveTopConditionalToGeneralPurposeRegister(); err != nil {
		return
	}

	dropValues, liveValues := locationStack.dropsLivesForInclusiveRange(r)

	// Frees all the registers used by drop target values.
	for i := range dropValues {
		dv := &dropValues[i]
		if dv.onRegister() {
			locationStack.releaseRegister(dv)
		}
	}

	// These registers are not nil if a live value of that type is currently located on the memory stack.
	// In order to migrate these values, we can use them below.
	gpTmp, vecTmp, err := getTemporariesForStackedLiveValues(c, liveValues)
	if err != nil {
		return err
	}

	// Reset the stack pointer below the end.
	locationStack.sp -= uint64(len(liveValues) + len(dropValues))

	// Push back the live values again.
	for i := range liveValues {
		live := &liveValues[i]
		migrateLiveValue(c, live, gpTmp, vecTmp)
	}
	return
}

// migrateLiveValue migrates the live value `live` into the top of the stack. It might be located on the stack
// and in that case, we have to load it into either `generalPurposeTmpReg` or `vectorTmpReg` temporarily, and
// write it back into the *new* stack location.
func migrateLiveValue(c compiler, live *runtimeValueLocation, generalPurposeTmpReg, vectorTmpReg asm.Register) {
	if live.valueType == runtimeValueTypeV128Hi {
		// Higher bits of vector was already handled together with the lower bits.
		return
	}

	previouslyOnStack := live.onStack()
	if previouslyOnStack {
		// If the value is on the stack, load the value on the old location into the temporary value,
		// and then write it back to the new memory location below.
		switch live.getRegisterType() {
		case registerTypeGeneralPurpose:
			live.setRegister(generalPurposeTmpReg)
		case registerTypeVector:
			live.setRegister(vectorTmpReg)
		}
		// Load the value into tmp.
		c.compileLoadValueOnStackToRegister(live)
	}

	var newLocation *runtimeValueLocation
	if live.valueType == runtimeValueTypeV128Lo {
		newLocation = c.pushVectorRuntimeValueLocationOnRegister(live.register)
	} else {
		newLocation = c.pushRuntimeValueLocationOnRegister(live.register, live.valueType)
	}

	if previouslyOnStack {
		// This case, the location is on the temporary register. Therefore,
		// we have to release the value there into the *new* memory location
		// so that the tmp can be used for subsequent live value migrations.
		c.compileReleaseRegisterToStack(newLocation)
	}
}

func getTemporariesForStackedLiveValues(c compiler, liveValues []runtimeValueLocation) (gpTmp, vecTmp asm.Register, err error) {
	gpTmp, vecTmp = asm.NilRegister, asm.NilRegister
	for i := range liveValues {
		l := &liveValues[i]
		if l.onStack() {
			if rt := l.getRegisterType(); rt == registerTypeGeneralPurpose && gpTmp == asm.NilRegister {
				gpTmp, err = c.allocateRegister(registerTypeGeneralPurpose)
				if err != nil {
					return
				}
			} else if rt == registerTypeVector && vecTmp == asm.NilRegister {
				vecTmp, err = c.allocateRegister(registerTypeVector)
				if err != nil {
					return
				}
			}
		}
	}
	return
}

// dropsLivesForInclusiveRange returns the live and drop target values for the given wazeroir.InclusiveRange.
func (v *runtimeValueLocationStack) dropsLivesForInclusiveRange(
	r wazeroir.InclusiveRange,
) (dropValues, liveValues []runtimeValueLocation) {
	// liveValues are must be pushed backed after dropping the target range.
	liveValues = v.stack[v.sp-uint64(r.Start) : v.sp]
	// dropValues are the values on the drop target range.
	dropValues = v.stack[v.sp-uint64(r.End)-1 : v.sp-uint64(r.Start)]
	return
}
