package vm

import (
	"fmt"

	"github.com/filecoin-project/go-state-types/network"

	vmr "github.com/filecoin-project/specs-actors/v7/actors/runtime"
	proof7 "github.com/filecoin-project/specs-actors/v7/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/ipfs/go-cid"

	"github.com/filecoin-project/lotus/build"
)

type GasCharge struct {
	Name  string
	Extra interface{}

	ComputeGas int64
	StorageGas int64

	VirtualCompute int64
	VirtualStorage int64
}

func (g GasCharge) Total() int64 {
	return g.ComputeGas + g.StorageGas
}
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 LegacyVM.
//
// 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 proof7.SealVerifyInfo) GasCharge
	OnVerifyAggregateSeals(aggregate proof7.AggregateSealVerifyProofAndInfos) GasCharge
	OnVerifyReplicaUpdate(update proof7.ReplicaUpdateInfo) GasCharge
	OnVerifyPost(info proof7.WindowPoStVerifyInfo) GasCharge
	OnVerifyConsensusFault() GasCharge
}

var priceListGenesis = pricelistV0{
	computeGasMulti: 1,
	storageGasMulti: 1000,

	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
	verifyAggregateSealBase:      0,
	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,
		},
	},
	verifyPostDiscount:   true,
	verifyConsensusFault: 495422,
}

var priceListCalico = pricelistV0{
	computeGasMulti: 1,
	storageGasMulti: 1300,

	onChainMessageComputeBase:    38863,
	onChainMessageStorageBase:    36,
	onChainMessageStoragePerByte: 1,

	onChainReturnValuePerByte: 1,

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

	ipldGetBase:    114617,
	ipldPutBase:    353640,
	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

	verifyAggregateSealPer: map[abi.RegisteredSealProof]int64{
		abi.RegisteredSealProof_StackedDrg32GiBV1_1: 449900,
		abi.RegisteredSealProof_StackedDrg64GiBV1_1: 359272,
	},
	verifyAggregateSealSteps: map[abi.RegisteredSealProof]stepCost{
		abi.RegisteredSealProof_StackedDrg32GiBV1_1: {
			{4, 103994170},
			{7, 112356810},
			{13, 122912610},
			{26, 137559930},
			{52, 162039100},
			{103, 210960780},
			{205, 318351180},
			{410, 528274980},
		},
		abi.RegisteredSealProof_StackedDrg64GiBV1_1: {
			{4, 102581240},
			{7, 110803030},
			{13, 120803700},
			{26, 134642130},
			{52, 157357890},
			{103, 203017690},
			{205, 304253590},
			{410, 509880640},
		},
	},

	verifyPostLookup: map[abi.RegisteredPoStProof]scalingCost{
		abi.RegisteredPoStProof_StackedDrgWindow512MiBV1: {
			flat:  117680921,
			scale: 43780,
		},
		abi.RegisteredPoStProof_StackedDrgWindow32GiBV1: {
			flat:  117680921,
			scale: 43780,
		},
		abi.RegisteredPoStProof_StackedDrgWindow64GiBV1: {
			flat:  117680921,
			scale: 43780,
		},
	},
	verifyPostDiscount:   false,
	verifyConsensusFault: 495422,

	verifyReplicaUpdate: 36316136,
}

// Prices are the price lists per starting epoch.
// For network v8 and onwards, this is disregarded; the pricelist is selected by network version.
var pricesByEpoch = map[abi.ChainEpoch]Pricelist{
	abi.ChainEpoch(0):                         &priceListGenesis,
	abi.ChainEpoch(build.UpgradeCalicoHeight): &priceListCalico,
}

// PricelistByEpochAndNetworkVersion finds the latest prices for the given epoch
func PricelistByEpochAndNetworkVersion(epoch abi.ChainEpoch, nv network.Version) Pricelist {
	if nv >= network.Version8 {
		return &priceListCalico
	}

	// 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 := pricesByEpoch[bestEpoch]
	for e, pl := range pricesByEpoch {
		// 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 proof7.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 proof7.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) (*vmr.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][]proof7.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)
}

func (ps pricedSyscalls) VerifyAggregateSeals(aggregate proof7.AggregateSealVerifyProofAndInfos) error {
	ps.chargeGas(ps.pl.OnVerifyAggregateSeals(aggregate))
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.VerifyAggregateSeals(aggregate)
}

func (ps pricedSyscalls) VerifyReplicaUpdate(update proof7.ReplicaUpdateInfo) error {
	ps.chargeGas(ps.pl.OnVerifyReplicaUpdate(update))
	defer ps.chargeGas(gasOnActorExec)

	return ps.under.VerifyReplicaUpdate(update)
}