// Copyright 2020 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

package wasmsolo

import (
	"bytes"
	"errors"

	"golang.org/x/xerrors"

	"github.com/iotaledger/wasp/packages/isc"
	"github.com/iotaledger/wasp/packages/kv/dict"
	"github.com/iotaledger/wasp/packages/solo"
	"github.com/iotaledger/wasp/packages/vm/gas"
	"github.com/iotaledger/wasp/packages/vm/sandbox"
	"github.com/iotaledger/wasp/packages/wasmvm/wasmhost"
	"github.com/iotaledger/wasp/packages/wasmvm/wasmlib/go/wasmlib"
	"github.com/iotaledger/wasp/packages/wasmvm/wasmlib/go/wasmlib/wasmrequests"
)

// SoloSandbox acts as a temporary host side of the WasmLib Sandbox interface.
// It acts as a change-resistant layer to wrap changes to the Solo environment,
// to limit bothering users of WasmLib as little as possible with those changes.
// Note that only those functions that are related to invocation of SC requests
// are actually necessary here. These sandbox functions will never be called
// other than through the SC function call interface generated by schema tool.
type SoloSandbox struct {
	ctx   *SoloContext
	cvt   wasmhost.WasmConvertor
	utils isc.Utils
}

func (s *SoloSandbox) Burn(burnCode gas.BurnCode, par ...uint64) {
	_ = burnCode
	_ = par
	// just do nothing
}

func (s *SoloSandbox) Budget() uint64 {
	panic("implement Budget")
}

var (
	_ wasmhost.ISandbox = new(SoloSandbox)
	_ isc.Gas           = new(SoloSandbox)
)

func NewSoloSandbox(ctx *SoloContext) *SoloSandbox {
	s := &SoloSandbox{ctx: ctx}
	s.utils = sandbox.NewUtils(s)
	return s
}

func (s *SoloSandbox) Call(funcNr int32, args []byte) []byte {
	s.ctx.Err = nil
	defer func() {
		r := recover()
		if r == nil {
			return
		}
		if s.ctx.Err != nil {
			s.ctx.Chain.Log().Infof("stacked error: %s", s.ctx.Err.Error())
		}
		switch errType := r.(type) {
		case error:
			s.ctx.Err = errType
		case string:
			s.ctx.Err = errors.New(errType)
		default:
			s.ctx.Err = xerrors.Errorf("RunScFunction: %v", errType)
		}
		s.ctx.Chain.Log().Infof("stolor error:: %s", s.ctx.Err.Error())
	}()
	switch funcNr {
	case wasmlib.FnCall:
		return s.fnCall(args)
	case wasmlib.FnLog:
		return s.fnLog(args)
	case wasmlib.FnPost:
		return s.fnPost(args)
	case wasmlib.FnUtilsBech32Decode:
		return s.fnUtilsBech32Decode(args)
	case wasmlib.FnUtilsBech32Encode:
		return s.fnUtilsBech32Encode(args)
	case wasmlib.FnUtilsHashName:
		return s.fnUtilsHashName(args)
	}
	panic("implement SoloSandbox.Call")
}

func (s *SoloSandbox) checkErr(err error) {
	if err != nil {
		panic(err.Error())
	}
}

func (s *SoloSandbox) Panicf(format string, args ...interface{}) {
	s.ctx.Chain.Log().Panicf(format, args...)
}

func (s *SoloSandbox) Tracef(format string, args ...interface{}) {
	s.ctx.Chain.Log().Debugf(format, args...)
}

func (s *SoloSandbox) postSync(contract, function string, params dict.Dict, allowance, transfer *isc.Allowance) []byte {
	ctx := s.ctx
	req := solo.NewCallParamsFromDict(contract, function, params)
	if allowance.IsEmpty() {
		allowance = transfer
	}
	req.WithAllowance(allowance)
	// Force a minimum transfer of 1 Million base tokens for storage deposit and some gas
	// excess can always be reclaimed from the chain account by the user
	// This also removes the silly requirement to transfer 1 base token
	if transfer.IsEmpty() && !ctx.offLedger {
		transfer = isc.NewAllowanceBaseTokens(1 * isc.Million)
	}
	if !transfer.IsEmpty() && transfer.Assets.BaseTokens < 1*isc.Million {
		transfer = transfer.Clone()
		transfer.Assets.BaseTokens = 1 * isc.Million
	}
	req.AddFungibleTokens(transfer.Assets)
	if len(transfer.NFTs) != 0 {
		if len(transfer.NFTs) != 1 {
			panic("cannot transfer multiple NFTs")
		}
		nftID := transfer.NFTs[0]
		if ctx.nfts == nil || ctx.nfts[nftID] == nil {
			panic("unknown nftID, did you use the wrong SoloContext?")
		}
		req.WithNFT(ctx.nfts[nftID])
	}
	req.WithMaxAffordableGasBudget()
	_ = wasmhost.Connect(ctx.wasmHostOld)
	var res dict.Dict
	if ctx.offLedger {
		ctx.offLedger = false
		res, ctx.Err = ctx.Chain.PostRequestOffLedger(req, ctx.keyPair)
	} else if !ctx.isRequest {
		ctx.Tx, res, ctx.Err = ctx.Chain.PostRequestSyncTx(req, ctx.keyPair)
	} else {
		ctx.isRequest = false
		ctx.Tx, _, ctx.Err = ctx.Chain.RequestFromParamsToLedger(req, nil)
		if ctx.Err == nil {
			ctx.Chain.Env.EnqueueRequests(ctx.Tx)
		}
	}
	_ = wasmhost.Connect(ctx.wc)
	ctx.UpdateGasFees()
	if ctx.Err != nil {
		return nil
	}
	return res.Bytes()
}

//////////////////// sandbox functions \\\\\\\\\\\\\\\\\\\\

func (s *SoloSandbox) fnCall(args []byte) []byte {
	ctx := s.ctx
	req := wasmrequests.NewCallRequestFromBytes(args)
	contract := s.cvt.IscHname(req.Contract)
	if contract != isc.Hn(ctx.scName) {
		s.Panicf("unknown contract: %s vs. %s", contract.String(), ctx.scName)
	}
	function := s.cvt.IscHname(req.Function)
	funcName := ctx.wc.FunctionFromCode(uint32(function))
	if funcName == "" {
		s.Panicf("unknown function: %s", function.String())
	}
	s.Tracef("CALL %s.%s", ctx.scName, funcName)
	params, err := dict.FromBytes(req.Params)
	s.checkErr(err)
	scAllowance := wasmlib.NewScAssets(req.Allowance)
	if !scAllowance.IsEmpty() {
		allowance := s.cvt.IscAllowance(scAllowance)
		return s.postSync(ctx.scName, funcName, params, allowance, nil)
	}

	_ = wasmhost.Connect(ctx.wasmHostOld)
	res, err := ctx.Chain.CallView(ctx.scName, funcName, params)
	_ = wasmhost.Connect(ctx.wc)
	ctx.Err = err
	if ctx.Err != nil {
		return nil
	}
	return res.Bytes()
}

func (s *SoloSandbox) fnChainID(_ []byte) []byte {
	return s.ctx.CurrentChainID().Bytes()
}

func (s *SoloSandbox) fnLog(args []byte) []byte {
	s.ctx.Chain.Log().Infof(string(args))
	return nil
}

func (s *SoloSandbox) fnPost(args []byte) []byte {
	req := wasmrequests.NewPostRequestFromBytes(args)
	if !bytes.Equal(req.ChainID.Bytes(), s.fnChainID(nil)) {
		s.Panicf("unknown chain id: %s", req.ChainID.String())
	}
	contract := s.cvt.IscHname(req.Contract)
	if contract != isc.Hn(s.ctx.scName) {
		s.Panicf("unknown contract: %s", contract.String())
	}
	function := s.cvt.IscHname(req.Function)
	funcName := s.ctx.wc.FunctionFromCode(uint32(function))
	if funcName == "" {
		s.Panicf("unknown function: %s", function.String())
	}
	s.Tracef("POST %s.%s", s.ctx.scName, funcName)
	params, err := dict.FromBytes(req.Params)
	s.checkErr(err)
	if req.Delay != 0 {
		s.Panicf("cannot delay solo post")
	}
	allowance := s.cvt.IscAllowance(wasmlib.NewScAssets(req.Allowance))
	transfer := s.cvt.IscAllowance(wasmlib.NewScAssets(req.Transfer))
	return s.postSync(s.ctx.scName, funcName, params, allowance, transfer)
}
