// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package eth

import (
	"math/big"
	"math/rand"
	"sync"

	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/event"
	"github.com/ethereum/go-ethereum/logger"
	"github.com/ethereum/go-ethereum/logger/glog"
)

const gpoProcessPastBlocks = 100

type blockPriceInfo struct {
	baseGasPrice *big.Int
}

type GasPriceOracle struct {
	eth                           *Ethereum
	chain                         *core.ChainManager
	pool                          *core.TxPool
	events                        event.Subscription
	blocks                        map[uint64]*blockPriceInfo
	firstProcessed, lastProcessed uint64
	lastBaseMutex                 sync.Mutex
	lastBase                      *big.Int
}

func NewGasPriceOracle(eth *Ethereum) (self *GasPriceOracle) {
	self = &GasPriceOracle{}
	self.blocks = make(map[uint64]*blockPriceInfo)
	self.eth = eth
	self.chain = eth.chainManager
	self.pool = eth.txPool
	self.events = eth.EventMux().Subscribe(
		core.ChainEvent{},
		core.ChainSplitEvent{},
		core.TxPreEvent{},
		core.TxPostEvent{},
	)
	self.processPastBlocks()
	go self.listenLoop()
	return
}

func (self *GasPriceOracle) processPastBlocks() {
	last := int64(-1)
	cblock := self.chain.CurrentBlock()
	if cblock != nil {
		last = int64(cblock.NumberU64())
	}
	first := int64(0)
	if last > gpoProcessPastBlocks {
		first = last - gpoProcessPastBlocks
	}
	self.firstProcessed = uint64(first)
	for i := first; i <= last; i++ {
		block := self.chain.GetBlockByNumber(uint64(i))
		if block != nil {
			self.processBlock(block)
		}
	}

}

func (self *GasPriceOracle) listenLoop() {
	for {
		ev, isopen := <-self.events.Chan()
		if !isopen {
			break
		}
		switch ev := ev.(type) {
		case core.ChainEvent:
			self.processBlock(ev.Block)
		case core.ChainSplitEvent:
			self.processBlock(ev.Block)
		case core.TxPreEvent:
		case core.TxPostEvent:
		}
	}
	self.events.Unsubscribe()
}

func (self *GasPriceOracle) processBlock(block *types.Block) {
	i := block.NumberU64()
	if i > self.lastProcessed {
		self.lastProcessed = i
	}

	lastBase := self.eth.GpoMinGasPrice
	bpl := self.blocks[i-1]
	if bpl != nil {
		lastBase = bpl.baseGasPrice
	}
	if lastBase == nil {
		return
	}

	var corr int
	lp := self.lowestPrice(block)
	if lp == nil {
		return
	}

	if lastBase.Cmp(lp) < 0 {
		corr = self.eth.GpobaseStepUp
	} else {
		corr = -self.eth.GpobaseStepDown
	}

	crand := int64(corr * (900 + rand.Intn(201)))
	newBase := new(big.Int).Mul(lastBase, big.NewInt(1000000+crand))
	newBase.Div(newBase, big.NewInt(1000000))

	bpi := self.blocks[i]
	if bpi == nil {
		bpi = &blockPriceInfo{}
		self.blocks[i] = bpi
	}
	bpi.baseGasPrice = newBase
	self.lastBaseMutex.Lock()
	self.lastBase = newBase
	self.lastBaseMutex.Unlock()

	glog.V(logger.Detail).Infof("Processed block #%v, base price is %v\n", block.NumberU64(), newBase.Int64())
}

// returns the lowers possible price with which a tx was or could have been included
func (self *GasPriceOracle) lowestPrice(block *types.Block) *big.Int {
	gasUsed := new(big.Int)

	receipts := self.eth.BlockProcessor().GetBlockReceipts(block.Hash())
	if len(receipts) > 0 {
		if cgu := receipts[len(receipts)-1].CumulativeGasUsed; cgu != nil {
			gasUsed = receipts[len(receipts)-1].CumulativeGasUsed
		}
	}

	if new(big.Int).Mul(gasUsed, big.NewInt(100)).Cmp(new(big.Int).Mul(block.GasLimit(),
		big.NewInt(int64(self.eth.GpoFullBlockRatio)))) < 0 {
		// block is not full, could have posted a tx with MinGasPrice
		return self.eth.GpoMinGasPrice
	}

	txs := block.Transactions()
	if len(txs) == 0 {
		return self.eth.GpoMinGasPrice
	}
	// block is full, find smallest gasPrice
	minPrice := txs[0].GasPrice()
	for i := 1; i < len(txs); i++ {
		price := txs[i].GasPrice()
		if price.Cmp(minPrice) < 0 {
			minPrice = price
		}
	}
	return minPrice
}

func (self *GasPriceOracle) SuggestPrice() *big.Int {
	self.lastBaseMutex.Lock()
	base := self.lastBase
	self.lastBaseMutex.Unlock()

	if base == nil {
		base = self.eth.GpoMinGasPrice
	}
	if base == nil {
		return big.NewInt(10000000000000) // apparently MinGasPrice is not initialized during some tests
	}

	baseCorr := new(big.Int).Mul(base, big.NewInt(int64(self.eth.GpobaseCorrectionFactor)))
	baseCorr.Div(baseCorr, big.NewInt(100))

	if baseCorr.Cmp(self.eth.GpoMinGasPrice) < 0 {
		return self.eth.GpoMinGasPrice
	}

	if baseCorr.Cmp(self.eth.GpoMaxGasPrice) > 0 {
		return self.eth.GpoMaxGasPrice
	}

	return baseCorr
}
