package compiler

import (
	"fmt"
	"go/ast"
	"go/constant"
	"go/types"
	"slices"

	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop/runtime"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/binding"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/emit"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

// inlineCall inlines call of n for function represented by f.
// Call `f(a,b)` for definition `func f(x,y int)` is translated to block:
//
//	{
//	   x := a
//	   y := b
//	   <inline body of f directly>
//	}
func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) {
	offSz := len(c.inlineContext)
	c.inlineContext = append(c.inlineContext, inlineContextSingle{
		labelOffset: len(c.labelList),
		returnLabel: c.newLabel(),
	})

	defer func() {
		c.labelList = c.labelList[:c.inlineContext[offSz].labelOffset]
		c.inlineContext = c.inlineContext[:offSz]
	}()

	pkg := c.packageCache[f.pkg.Path()]
	sig := c.typeOf(n.Fun).(*types.Signature)

	hasVarArgs := !n.Ellipsis.IsValid()
	eventParams := c.processStdlibCall(f, n.Args, !hasVarArgs)

	// When inlined call is used during global initialization
	// there is no func scope, thus this if.
	if c.scope == nil {
		c.scope = &funcScope{}
		c.scope.vars.newScope()
		defer func() {
			c.globalInlineCount = max(c.globalInlineCount, c.scope.vars.localsCnt)
			c.scope = nil
		}()
	}

	// Arguments need to be walked with the current scope,
	// while stored in the new.
	oldScope := c.scope.vars.locals
	c.scope.vars.newScope()
	newScope := slices.Clone(c.scope.vars.locals)
	defer c.scope.vars.dropScope()

	if f.decl.Recv != nil {
		c.scope.vars.locals = newScope
		name := f.decl.Recv.List[0].Names[0].Name
		c.scope.vars.addAlias(name, -1, unspecifiedVarIndex, &varContext{
			importMap: c.importMap,
			expr:      f.selector,
			scope:     oldScope,
		})
	}
	needPack := sig.Variadic() && hasVarArgs
	for i := range n.Args {
		c.scope.vars.locals = oldScope
		// true if normal arg or var arg is `slice...`
		needStore := i < sig.Params().Len()-1 || !sig.Variadic() || !hasVarArgs
		if !needStore {
			break
		}
		name := sig.Params().At(i).Name()
		if !c.hasCalls(n.Args[i]) {
			// If argument contains no calls, we save context and traverse the expression
			// when argument is emitted.
			c.scope.vars.locals = newScope
			c.scope.vars.addAlias(name, -1, unspecifiedVarIndex, &varContext{
				importMap: c.importMap,
				expr:      n.Args[i],
				scope:     oldScope,
			})
			continue
		}

		ast.Walk(c, n.Args[i])
		c.scope.vars.locals = newScope
		c.scope.newLocal(name)
		c.emitStoreVar("", name)
	}

	if needPack {
		// traverse variadic args and pack them
		// if they are provided directly i.e. without `...`
		c.scope.vars.locals = oldScope
		for i := sig.Params().Len() - 1; i < len(n.Args); i++ {
			ast.Walk(c, n.Args[i])
			// In case of runtime.Notify, its arguments need to be converted to proper type.
			// i's initial value is 1 (variadic args start).
			if eventParams != nil && eventParams[i-1] != nil {
				c.emitConvert(*eventParams[i-1])
			}
		}
		c.scope.vars.locals = newScope
		c.packVarArgs(n, sig)
		name := sig.Params().At(sig.Params().Len() - 1).Name()
		c.scope.newLocal(name)
		c.emitStoreVar("", name)
	}

	c.pkgInfoInline = append(c.pkgInfoInline, pkg)
	oldMap := c.importMap
	oldDefers := c.scope.deferStack
	c.scope.deferStack = nil
	c.fillImportMap(f.file, pkg)
	ast.Inspect(f.decl, c.scope.analyzeVoidCalls)
	ast.Walk(c, f.decl.Body)
	c.setLabel(c.inlineContext[offSz].returnLabel)
	if c.scope.voidCalls[n] {
		for range f.decl.Type.Results.NumFields() {
			emit.Opcodes(c.prog.BinWriter, opcode.DROP)
		}
	}
	c.processDefers()
	c.scope.deferStack = oldDefers
	c.importMap = oldMap
	c.pkgInfoInline = c.pkgInfoInline[:len(c.pkgInfoInline)-1]
}

func (c *codegen) processStdlibCall(f *funcScope, args []ast.Expr, hasEllipsis bool) []*stackitem.Type {
	if f == nil {
		return nil
	}

	var eventParams []*stackitem.Type
	if f.pkg.Path() == interopPrefix+"/runtime" && (f.name == "Notify" || f.name == "Log") {
		eventParams = c.processNotify(f, args, hasEllipsis)
	}

	if f.pkg.Path() == interopPrefix+"/contract" && f.name == "Call" {
		c.processContractCall(f, args)
	}
	return eventParams
}

// processNotify checks whether notification emitting rules are met and returns expected
// notification signature if found.
func (c *codegen) processNotify(f *funcScope, args []ast.Expr, hasEllipsis bool) []*stackitem.Type {
	if c.scope != nil && c.isVerifyFunc(c.scope.decl) &&
		c.scope.pkg == c.mainPkg.Types && (c.buildInfo.options == nil || !c.buildInfo.options.NoEventsCheck) {
		c.prog.Err = fmt.Errorf("runtime.%s is not allowed in `Verify`", f.name)
		return nil
	}

	if f.name == "Log" {
		return nil
	}

	// Sometimes event name is stored in a var. Or sometimes event args are provided
	// via ellipses (`slice...`).
	// Skip in this case. Also, don't enforce runtime.Notify parameters conversion.
	tv := c.typeAndValueOf(args[0])
	if tv.Value == nil || hasEllipsis {
		return nil
	}

	params := make([]DebugParam, 0, len(args[1:]))
	vParams := make([]*stackitem.Type, 0, len(args[1:]))
	// extMap holds the extended parameter types used for the given event call.
	// It will be unified with the common extMap later during bindings config
	// generation.
	extMap := make(map[string]binding.ExtendedType)
	for _, p := range args[1:] {
		st, vt, over, extT := c.scAndVMTypeFromExpr(p, extMap)
		params = append(params, DebugParam{
			Name:         "", // Parameter name will be filled in several lines below if the corresponding event exists in the buildinfo.options.
			Type:         vt.String(),
			RealType:     over,
			ExtendedType: extT,
			TypeSC:       st,
		})
		vParams = append(vParams, &vt)
	}

	name := constant.StringVal(tv.Value)
	if len(name) > runtime.MaxEventNameLen {
		c.prog.Err = fmt.Errorf("event name '%s' should be less than %d",
			name, runtime.MaxEventNameLen)
		return nil
	}
	var eventFound bool
	if c.buildInfo.options != nil && c.buildInfo.options.ContractEvents != nil {
		for _, e := range c.buildInfo.options.ContractEvents {
			if e.Name == name && len(e.Parameters) == len(vParams) {
				eventFound = true
				for i, scParam := range e.Parameters {
					params[i].Name = scParam.Name
					if !c.buildInfo.options.NoEventsCheck {
						expectedType := scParam.Type.ConvertToStackitemType()
						// No need to cast if the desired type is unknown.
						if expectedType == stackitem.AnyT ||
							// Do not cast if desired type is Interop, the actual type is likely to be Any, leave the resolving to runtime.Notify.
							expectedType == stackitem.InteropT ||
							// No need to cast if actual parameter type matches the desired one.
							*vParams[i] == expectedType ||
							// expectedType doesn't contain Buffer anyway, but if actual variable type is Buffer,
							// then runtime.Notify will convert it to ByteArray automatically, thus no need to emit conversion code.
							(*vParams[i] == stackitem.BufferT && expectedType == stackitem.ByteArrayT) {
							vParams[i] = nil
						} else {
							// For other cases the conversion code will be emitted using vParams...
							vParams[i] = &expectedType
							// ...thus, update emitted notification info in advance.
							params[i].Type = scParam.Type.String()
							params[i].TypeSC = scParam.Type
						}
					}
				}
			}
		}
	}
	c.emittedEvents[name] = append(c.emittedEvents[name], EmittedEventInfo{
		ExtTypes: extMap,
		Params:   params,
	})
	// Do not enforce perfect expected/actual events match on this step, the final
	// check will be performed after compilation if --no-events option is off.
	if eventFound && !c.buildInfo.options.NoEventsCheck {
		return vParams
	}
	return nil
}

func (c *codegen) processContractCall(f *funcScope, args []ast.Expr) {
	var u util.Uint160

	// For stdlib calls it is `interop.Hash160(constHash)`
	// so we can determine hash at compile-time.
	ce, ok := args[0].(*ast.CallExpr)
	if ok && len(ce.Args) == 1 {
		// Ensure this is a type conversion, not a simple invoke.
		se, ok := ce.Fun.(*ast.SelectorExpr)
		if ok {
			name, _ := c.getFuncNameFromSelector(se)
			if _, ok := c.funcs[name]; !ok {
				value := c.typeAndValueOf(ce.Args[0]).Value
				if value != nil {
					s := constant.StringVal(value)
					copy(u[:], s) // constant must be big-endian
				}
			}
		}
	}

	value := c.typeAndValueOf(args[1]).Value
	if value == nil {
		return
	}

	method := constant.StringVal(value)

	value = c.typeAndValueOf(args[2]).Value
	if value == nil {
		return
	}

	flag, _ := constant.Uint64Val(value)
	c.appendInvokedContract(u, method, flag)
}

func (c *codegen) appendInvokedContract(u util.Uint160, method string, flag uint64) {
	currLst := c.invokedContracts[u]
	if slices.Contains(currLst, method) {
		return
	}

	if flag&uint64(callflag.WriteStates|callflag.AllowNotify) != 0 {
		c.invokedContracts[u] = append(currLst, method)
	}
}

// hasCalls returns true if expression contains any calls.
// We uses this as a rough heuristic to determine if expression calculation
// has any side-effects.
func (c *codegen) hasCalls(expr ast.Expr) bool {
	var has bool
	ast.Inspect(expr, func(n ast.Node) bool {
		ce, ok := n.(*ast.CallExpr)
		if !has && ok {
			isFunc := true
			fun, ok := ce.Fun.(*ast.Ident)
			if ok {
				_, isFunc = c.getFuncFromIdent(fun)
			} else {
				var sel *ast.SelectorExpr
				sel, ok = ce.Fun.(*ast.SelectorExpr)
				if ok {
					name, _ := c.getFuncNameFromSelector(sel)
					_, isFunc = c.funcs[name]
					fun = sel.Sel
				}
			}
			has = isFunc || fun.Obj != nil && (fun.Obj.Kind == ast.Var || fun.Obj.Kind == ast.Fun)
		}
		return !has
	})
	return has
}
