package runtime

import (
	"encoding/binary"
	"errors"
	"math/big"
	"slices"

	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/address"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"github.com/twmb/murmur3"
)

// GasLeft returns the remaining amount of GAS.
func GasLeft(ic *interop.Context) error {
	ic.VM.Estack().PushItem(stackitem.NewBigInteger(ic.VM.GasLeft()))
	return nil
}

// GetNotifications returns notifications emitted in the current execution context.
func GetNotifications(ic *interop.Context) error {
	item := ic.VM.Estack().Pop().Item()
	notifications := ic.Notifications
	if _, ok := item.(stackitem.Null); !ok {
		b, err := item.TryBytes()
		if err != nil {
			return err
		}
		u, err := util.Uint160DecodeBytesBE(b)
		if err != nil {
			return err
		}
		notifications = []state.NotificationEvent{}
		for i := range ic.Notifications {
			if ic.Notifications[i].ScriptHash.Equals(u) {
				notifications = append(notifications, ic.Notifications[i])
			}
		}
	}
	if len(notifications) > vm.MaxStackSize {
		return errors.New("too many notifications")
	}
	arr := stackitem.NewArray(make([]stackitem.Item, 0, len(notifications)))
	for i := range notifications {
		ev := stackitem.NewArray([]stackitem.Item{
			stackitem.NewByteArray(notifications[i].ScriptHash.BytesBE()),
			stackitem.Make(notifications[i].Name),
			notifications[i].Item,
		})
		arr.Append(ev)
	}
	ic.VM.Estack().PushItem(arr)
	return nil
}

// GetInvocationCounter returns how many times the current contract has been invoked during the current tx execution.
func GetInvocationCounter(ic *interop.Context) error {
	currentScriptHash := ic.VM.GetCurrentScriptHash()
	count, ok := ic.Invocations[currentScriptHash]
	if !ok {
		count = 1
		ic.Invocations[currentScriptHash] = count
	}
	ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(count))))
	return nil
}

// GetAddressVersion returns the address version of the current protocol.
func GetAddressVersion(ic *interop.Context) error {
	ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(address.NEO3Prefix))))
	return nil
}

// GetNetwork returns chain network number.
func GetNetwork(ic *interop.Context) error {
	m := ic.Chain.GetConfig().Magic
	ic.VM.Estack().PushItem(stackitem.NewBigInteger(big.NewInt(int64(m))))
	return nil
}

// GetRandom returns pseudo-random number which depends on block nonce and transaction hash.
func GetRandom(ic *interop.Context) error {
	var (
		price int64
		seed  = ic.Network
	)
	isHF := ic.IsHardforkEnabled(config.HFAspidochelone)
	if isHF {
		price = 1 << 13
		seed += ic.GetRandomCounter
		ic.GetRandomCounter++
	} else {
		price = 1 << 4
	}
	res := murmur128(ic.NonceData[:], seed)
	if !isHF {
		ic.NonceData = [interop.ContextNonceDataLen]byte(res)
	}
	if !ic.VM.AddPicoGas(ic.BaseExecFee() * price) {
		return errors.New("gas limit exceeded")
	}
	// Resulting data is interpreted as an unsigned LE integer.
	slices.Reverse(res)
	ic.VM.Estack().PushItem(stackitem.NewBigInteger(new(big.Int).SetBytes(res)))
	return nil
}

func murmur128(data []byte, seed uint32) []byte {
	h1, h2 := murmur3.SeedSum128(uint64(seed), uint64(seed), data)
	result := make([]byte, 16)
	binary.LittleEndian.PutUint64(result, h1)
	binary.LittleEndian.PutUint64(result[8:], h2)
	return result
}
