package vm

import (
	"fmt"

	"github.com/filecoin-project/specs-actors/actors/runtime/proof"

	"github.com/filecoin-project/go-address"
	addr "github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/go-state-types/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 = 1000
	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() 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 proof.SealVerifyInfo) GasCharge
	OnVerifyPost(info proof.WindowPoStVerifyInfo) GasCharge
	OnVerifyConsensusFault() GasCharge
}

var prices = map[abi.ChainEpoch]Pricelist{
	abi.ChainEpoch(0): &pricelistV0{
		onChainMessageComputeBase:    38863,
		onChainMessageStorageBase:    36,
		onChainMessageStoragePerByte: 1,

		onChainReturnValuePerByte: 1,

		sendBase:                29233,
		sendTransferFunds:       27500,
		sendTransferOnlyPremium: 159672,
		sendInvokeMethod:        -5377,

		ipldGetBase:    75242,
		ipldPutBase:    84070,
		ipldPutPerByte: 1,

		createActorCompute: 1108454,
		createActorStorage: 36 + 40,
		deleteActor:        -(36 + 40), // -createActorStorage

		verifySignature: map[crypto.SigType]int64{
			crypto.SigTypeBLS:       16598605,
			crypto.SigTypeSecp256k1: 1637292,
		},

		hashingBase:                  31355,
		computeUnsealedSectorCidBase: 98647,
		verifySealBase:               2000, // TODO gas , it VerifySeal syscall is not used
		verifyPostLookup: map[abi.RegisteredPoStProof]scalingCost{
			abi.RegisteredPoStProof_StackedDrgWindow512MiBV1: {
				flat:  123861062,
				scale: 9226981,
			},
			abi.RegisteredPoStProof_StackedDrgWindow32GiBV1: {
				flat:  748593537,
				scale: 85639,
			},
			abi.RegisteredPoStProof_StackedDrgWindow64GiBV1: {
				flat:  748593537,
				scale: 85639,
			},
		},
		verifyConsensusFault: 495422,
	},
}

// 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 proof.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 proof.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][]proof.SealVerifyInfo) (map[address.Address][]bool, error) {
	count := int64(0)
	for _, svis := range inp {
		count += int64(len(svis))
	}

	gasChargeSum := newGasCharge("BatchVerifySeals", 0, 0)
	gasChargeSum = gasChargeSum.WithExtra(count).WithVirtual(15075005*count+899741502, 0)
	ps.chargeGas(gasChargeSum) // real gas charged by actors
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.BatchVerifySeals(inp)
}