package vm

import (
	"fmt"

	"github.com/filecoin-project/go-address"
	addr "github.com/filecoin-project/go-address"
	"github.com/filecoin-project/specs-actors/actors/abi"
	"github.com/filecoin-project/specs-actors/actors/crypto"
	"github.com/filecoin-project/specs-actors/actors/runtime"
	vmr "github.com/filecoin-project/specs-actors/actors/runtime"
	"github.com/ipfs/go-cid"
)

const (
	GasStorageMulti = 1
	GasComputeMulti = 1
)

type GasCharge struct {
	Name  string
	Extra interface{}

	ComputeGas int64
	StorageGas int64

	VirtualCompute int64
	VirtualStorage int64
}

func (g GasCharge) Total() int64 {
	return g.ComputeGas*GasComputeMulti + g.StorageGas*GasStorageMulti
}
func (g GasCharge) WithVirtual(compute, storage int64) GasCharge {
	out := g
	out.VirtualCompute = compute
	out.VirtualStorage = storage
	return out
}

func (g GasCharge) WithExtra(extra interface{}) GasCharge {
	out := g
	out.Extra = extra
	return out
}

func newGasCharge(name string, computeGas int64, storageGas int64) GasCharge {
	return GasCharge{
		Name:       name,
		ComputeGas: computeGas,
		StorageGas: storageGas,
	}
}

// Pricelist provides prices for operations in the VM.
//
// Note: this interface should be APPEND ONLY since last chain checkpoint
type Pricelist interface {
	// OnChainMessage returns the gas used for storing a message of a given size in the chain.
	OnChainMessage(msgSize int) GasCharge
	// OnChainReturnValue returns the gas used for storing the response of a message in the chain.
	OnChainReturnValue(dataSize int) GasCharge

	// OnMethodInvocation returns the gas used when invoking a method.
	OnMethodInvocation(value abi.TokenAmount, methodNum abi.MethodNum) GasCharge

	// OnIpldGet returns the gas used for storing an object
	OnIpldGet(dataSize int) GasCharge
	// OnIpldPut returns the gas used for storing an object
	OnIpldPut(dataSize int) GasCharge

	// OnCreateActor returns the gas used for creating an actor
	OnCreateActor() GasCharge
	// OnDeleteActor returns the gas used for deleting an actor
	OnDeleteActor() GasCharge

	OnVerifySignature(sigType crypto.SigType, planTextSize int) (GasCharge, error)
	OnHashing(dataSize int) GasCharge
	OnComputeUnsealedSectorCid(proofType abi.RegisteredSealProof, pieces []abi.PieceInfo) GasCharge
	OnVerifySeal(info abi.SealVerifyInfo) GasCharge
	OnVerifyPost(info abi.WindowPoStVerifyInfo) GasCharge
	OnVerifyConsensusFault() GasCharge
}

var prices = map[abi.ChainEpoch]Pricelist{
	abi.ChainEpoch(0): &pricelistV0{
		onChainMessageBase:        0,
		onChainMessagePerByte:     2,
		onChainReturnValuePerByte: 8,
		sendBase:                  5,
		sendTransferFunds:         5,
		sendInvokeMethod:          10,
		ipldGetBase:               10,
		ipldGetPerByte:            1,
		ipldPutBase:               20,
		ipldPutPerByte:            2,
		createActorBase:           40, // IPLD put + 20
		createActorExtra:          500,
		deleteActor:               -500, // -createActorExtra
		// Dragons: this cost is not persistable, create a LinearCost{a,b} struct that has a `.Cost(x) -> ax + b`
		verifySignature: map[crypto.SigType]func(int64) int64{
			crypto.SigTypeBLS:       func(x int64) int64 { return 3*x + 2 },
			crypto.SigTypeSecp256k1: func(x int64) int64 { return 3*x + 2 },
		},
		hashingBase:                  5,
		hashingPerByte:               2,
		computeUnsealedSectorCidBase: 100,
		verifySealBase:               2000,
		verifyPostBase:               700,
		verifyConsensusFault:         10,
	},
}

// PricelistByEpoch finds the latest prices for the given epoch
func PricelistByEpoch(epoch abi.ChainEpoch) Pricelist {
	// since we are storing the prices as map or epoch to price
	// we need to get the price with the highest epoch that is lower or equal to the `epoch` arg
	bestEpoch := abi.ChainEpoch(0)
	bestPrice := prices[bestEpoch]
	for e, pl := range prices {
		// if `e` happened after `bestEpoch` and `e` is earlier or equal to the target `epoch`
		if e > bestEpoch && e <= epoch {
			bestEpoch = e
			bestPrice = pl
		}
	}
	if bestPrice == nil {
		panic(fmt.Sprintf("bad setup: no gas prices available for epoch %d", epoch))
	}
	return bestPrice
}

type pricedSyscalls struct {
	under     vmr.Syscalls
	pl        Pricelist
	chargeGas func(GasCharge)
}

// Verifies that a signature is valid for an address and plaintext.
func (ps pricedSyscalls) VerifySignature(signature crypto.Signature, signer addr.Address, plaintext []byte) error {
	c, err := ps.pl.OnVerifySignature(signature.Type, len(plaintext))
	if err != nil {
		return err
	}
	ps.chargeGas(c)
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.VerifySignature(signature, signer, plaintext)
}

// Hashes input data using blake2b with 256 bit output.
func (ps pricedSyscalls) HashBlake2b(data []byte) [32]byte {
	ps.chargeGas(ps.pl.OnHashing(len(data)))
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.HashBlake2b(data)
}

// Computes an unsealed sector CID (CommD) from its constituent piece CIDs (CommPs) and sizes.
func (ps pricedSyscalls) ComputeUnsealedSectorCID(reg abi.RegisteredSealProof, pieces []abi.PieceInfo) (cid.Cid, error) {
	ps.chargeGas(ps.pl.OnComputeUnsealedSectorCid(reg, pieces))
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.ComputeUnsealedSectorCID(reg, pieces)
}

// Verifies a sector seal proof.
func (ps pricedSyscalls) VerifySeal(vi abi.SealVerifyInfo) error {
	ps.chargeGas(ps.pl.OnVerifySeal(vi))
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.VerifySeal(vi)
}

// Verifies a proof of spacetime.
func (ps pricedSyscalls) VerifyPoSt(vi abi.WindowPoStVerifyInfo) error {
	ps.chargeGas(ps.pl.OnVerifyPost(vi))
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.VerifyPoSt(vi)
}

// Verifies that two block headers provide proof of a consensus fault:
// - both headers mined by the same actor
// - headers are different
// - first header is of the same or lower epoch as the second
// - at least one of the headers appears in the current chain at or after epoch `earliest`
// - the headers provide evidence of a fault (see the spec for the different fault types).
// The parameters are all serialized block headers. The third "extra" parameter is consulted only for
// the "parent grinding fault", in which case it must be the sibling of h1 (same parent tipset) and one of the
// blocks in the parent of h2 (i.e. h2's grandparent).
// Returns nil and an error if the headers don't prove a fault.
func (ps pricedSyscalls) VerifyConsensusFault(h1 []byte, h2 []byte, extra []byte) (*runtime.ConsensusFault, error) {
	ps.chargeGas(ps.pl.OnVerifyConsensusFault())
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.VerifyConsensusFault(h1, h2, extra)
}

func (ps pricedSyscalls) BatchVerifySeals(inp map[address.Address][]abi.SealVerifyInfo) (map[address.Address][]bool, error) {
	var gasChargeSum GasCharge
	gasChargeSum.Name = "BatchVerifySeals"
	count := int64(0)
	for _, svis := range inp {
		count += int64(len(svis))
	}
	gasChargeSum = gasChargeSum.WithExtra(count).WithVirtual(129778623*count+716683250, 0)
	ps.chargeGas(gasChargeSum) // TODO: this is only called by the cron actor. Should we even charge gas?
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.BatchVerifySeals(inp)
}