package vm

import (
	"bytes"
	"context"
	"encoding/binary"
	"runtime/debug"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/specs-actors/actors/abi"
	"github.com/filecoin-project/specs-actors/actors/abi/big"
	"github.com/filecoin-project/specs-actors/actors/crypto"
	vmr "github.com/filecoin-project/specs-actors/actors/runtime"
	"github.com/filecoin-project/specs-actors/actors/runtime/exitcode"
	"github.com/filecoin-project/specs-actors/actors/util/adt"
	"github.com/ipfs/go-cid"
	cbg "github.com/whyrusleeping/cbor-gen"

	"github.com/filecoin-project/lotus/chain/actors/aerrors"
	"github.com/filecoin-project/lotus/chain/types"
)

type runtimeShim struct {
	vmctx types.VMContext
}

func (rs *runtimeShim) ResolveAddress(address address.Address) (ret address.Address, ok bool) {
	r, err := rs.vmctx.LookupID(address)
	if err != nil { // TODO: check notfound
		rs.Abortf(exitcode.ErrPlaceholder, "resolve address: %v", err)
	}
	return r, true
}

func (rs *runtimeShim) Get(c cid.Cid, o vmr.CBORUnmarshaler) bool {
	err := rs.vmctx.Storage().Get(c, o)
	if err != nil { // todo: not found
		rs.Abortf(exitcode.ErrPlaceholder, "storage get: %v", err)
	}
	return true
}

func (rs *runtimeShim) Put(x vmr.CBORMarshaler) cid.Cid {
	c, err := rs.vmctx.Storage().Put(x)
	if err != nil {
		rs.Abortf(exitcode.ErrPlaceholder, "storage put: %v", err) // todo: spec code?
	}
	return c
}

var _ vmr.Runtime = (*runtimeShim)(nil)

func (rs *runtimeShim) shimCall(f func() interface{}) (rval []byte, aerr aerrors.ActorError) {
	defer func() {
		if r := recover(); r != nil {
			if ar, ok := r.(aerrors.ActorError); ok {
				log.Warn("VM.Call failure: ", ar)
				debug.PrintStack()
				aerr = ar
				return
			}
			debug.PrintStack()
			log.Errorf("ERROR")
			aerr = aerrors.Newf(1, "spec actors failure: %s", r)
		}
	}()

	ret := f()
	switch ret := ret.(type) {
	case []byte:
		return ret, nil
	case *adt.EmptyValue:
		return nil, nil
	case cbg.CBORMarshaler:
		buf := new(bytes.Buffer)
		if err := ret.MarshalCBOR(buf); err != nil {
			return nil, aerrors.Absorb(err, 2, "failed to marshal response to cbor")
		}
		return buf.Bytes(), nil
	case nil:
		return nil, nil
	default:
		return nil, aerrors.New(3, "could not determine type for response from call")
	}
}

func (rs *runtimeShim) Message() vmr.Message {
	var err error

	rawm := *rs.vmctx.Message() // TODO: normalize addresses earlier
	rawm.From, err = rs.vmctx.LookupID(rawm.From)
	if err != nil {
		rs.Abortf(exitcode.ErrPlaceholder, "resolve from address: %v", err)
	}

	rawm.To, err = rs.vmctx.LookupID(rawm.To)
	if err != nil {
		rs.Abortf(exitcode.ErrPlaceholder, "resolve to address: %v", err)
	}

	return &rawm
}

func (rs *runtimeShim) ValidateImmediateCallerAcceptAny() {
	return
}

func (rs *runtimeShim) CurrentBalance() abi.TokenAmount {
	b, err := rs.vmctx.GetBalance(rs.vmctx.Message().To)
	if err != nil {
		rs.Abortf(exitcode.ExitCode(err.RetCode()), "get current balance: %v", err)
	}
	return b
}

func (rs *runtimeShim) GetActorCodeCID(addr address.Address) (ret cid.Cid, ok bool) {
	ret, err := rs.vmctx.ActorCodeCID(addr)
	if err != nil {
		// todo: notfound
		rs.Abortf(exitcode.ErrPlaceholder, "%v", err)
	}

	return ret, true
}

func (rs *runtimeShim) GetRandomness(personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte) abi.Randomness {
	r, err := rs.vmctx.GetRandomness(personalization, randEpoch, entropy)
	if err != nil {
		rs.Abortf(exitcode.SysErrInternal, "getting randomness: %v", err)
	}

	return r
}

func (rs *runtimeShim) Store() vmr.Store {
	return rs
}

func (rs *runtimeShim) NewActorAddress() address.Address {
	var b bytes.Buffer
	if err := rs.ImmediateCaller().MarshalCBOR(&b); err != nil { // todo: spec says cbor; why not just bytes?
		rs.Abortf(exitcode.ErrSerialization, "writing caller address into a buffer: %v", err)
	}

	var err error
	st, err := rs.vmctx.StateTree()
	if err != nil {
		rs.Abortf(exitcode.SysErrInternal, "getting statetree: %v", err)
	}
	act, err := st.GetActor(rs.vmctx.Origin())
	if err != nil {
		rs.Abortf(exitcode.SysErrInternal, "getting top level actor: %v", err)
	}

	if err := binary.Write(&b, binary.BigEndian, act.Nonce); err != nil {
		rs.Abortf(exitcode.ErrSerialization, "writing nonce address into a buffer: %v", err)
	}
	if err := binary.Write(&b, binary.BigEndian, uint64(0)); err != nil { // TODO: expose on vm
		rs.Abortf(exitcode.ErrSerialization, "writing callSeqNum address into a buffer: %v", err)
	}
	addr, err := address.NewActorAddress(b.Bytes())
	if err != nil {
		rs.Abortf(exitcode.ErrSerialization, "create actor address: %v", err)
	}

	return addr
}

func (rs *runtimeShim) CreateActor(codeId cid.Cid, address address.Address) {
	var err error
	st, err := rs.vmctx.StateTree()
	if err != nil {
		rs.Abortf(exitcode.SysErrInternal, "getting statetree: %v", err)
	}

	err = st.SetActor(address, &types.Actor{
		Code:    codeId,
		Head:    EmptyObjectCid,
		Nonce:   0,
		Balance: big.Zero(),
	})
	if err != nil {
		rs.Abortf(exitcode.SysErrInternal, "creating actor entry: %v", err)
	}

	return
}

func (rs *runtimeShim) DeleteActor() {
	panic("implement me")
}

func (rs *runtimeShim) Syscalls() vmr.Syscalls {
	return rs.vmctx.Sys()
}

func (rs *runtimeShim) StartSpan(name string) vmr.TraceSpan {
	panic("implement me")
}

func (rs *runtimeShim) ValidateImmediateCallerIs(as ...address.Address) {
	imm, err := rs.vmctx.LookupID(rs.vmctx.Message().From)
	if err != nil {
		rs.Abortf(exitcode.ErrIllegalState, "couldn't resolve immediate caller")
	}

	for _, a := range as {
		if imm == a {
			return
		}
	}
	rs.Abortf(exitcode.ErrForbidden, "caller %s is not one of %s", rs.vmctx.Message().From, as)
}

func (rs *runtimeShim) ImmediateCaller() address.Address {
	return rs.vmctx.Message().From
}

func (rs *runtimeShim) Context() context.Context {
	return rs.vmctx.Context()
}

func (rs *runtimeShim) Abortf(code exitcode.ExitCode, msg string, args ...interface{}) {
	panic(aerrors.NewfSkip(2, uint8(code), msg, args...))
}

func (rs *runtimeShim) AbortStateMsg(msg string) {
	panic(aerrors.NewfSkip(3, 101, msg))
}

func (rs *runtimeShim) ValidateImmediateCallerType(...cid.Cid) {
	log.Info("validate caller type is dumb")
}

func (rs *runtimeShim) CurrEpoch() abi.ChainEpoch {
	return rs.vmctx.BlockHeight()
}

type dumbWrapperType struct {
	val []byte
}

func (dwt *dumbWrapperType) Into(um vmr.CBORUnmarshaler) error {
	return um.UnmarshalCBOR(bytes.NewReader(dwt.val))
}

func (rs *runtimeShim) Send(to address.Address, method abi.MethodNum, m vmr.CBORMarshaler, value abi.TokenAmount) (vmr.SendReturn, exitcode.ExitCode) {
	var params []byte
	if m != nil {
		buf := new(bytes.Buffer)
		if err := m.MarshalCBOR(buf); err != nil {
			rs.Abortf(exitcode.SysErrInvalidParameters, "failed to marshal input parameters: %s", err)
		}
		params = buf.Bytes()
	}

	ret, err := rs.vmctx.Send(to, method, types.BigInt(value), params)
	if err != nil {
		if err.IsFatal() {
			panic(err)
		}
		log.Warnf("vmctx send failed: to: %s, method: %d: ret: %d, err: %s", to, method, ret, err)
		return nil, exitcode.ExitCode(err.RetCode())
	}
	return &dumbWrapperType{ret}, 0
}

func (rs *runtimeShim) State() vmr.StateHandle {
	return &shimStateHandle{rs: rs}
}

type shimStateHandle struct {
	rs *runtimeShim
}

func (ssh *shimStateHandle) Create(obj vmr.CBORMarshaler) {
	c, err := ssh.rs.vmctx.Storage().Put(obj)
	if err != nil {
		panic(err)
	}
	if err := ssh.rs.vmctx.Storage().Commit(EmptyObjectCid, c); err != nil {
		panic(err)
	}
}

func (ssh *shimStateHandle) Readonly(obj vmr.CBORUnmarshaler) {
	if err := ssh.rs.vmctx.Storage().Get(ssh.rs.vmctx.Storage().GetHead(), obj); err != nil {
		panic(err)
	}
}

func (ssh *shimStateHandle) Transaction(obj vmr.CBORer, f func() interface{}) interface{} {
	head := ssh.rs.vmctx.Storage().GetHead()
	if err := ssh.rs.vmctx.Storage().Get(head, obj); err != nil {
		panic(err)
	}

	out := f()

	c, err := ssh.rs.vmctx.Storage().Put(obj)
	if err != nil {
		panic(err)
	}
	if err := ssh.rs.vmctx.Storage().Commit(head, c); err != nil {
		panic(err)
	}

	return out
}