// Copyright (c) 2020 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package mining

import (
	"github.com/decred/dcrd/chaincfg/chainhash"
	"github.com/decred/dcrd/dcrutil/v4"
)

const (
	// ancestorTrackingLimit is the maximum number of ancestors a transaction
	// can have ancestor stats calculated for.
	ancestorTrackingLimit = 25
)

// defaultAncestorStats is the default-state of ancestor stats for a transaction
// that has no ancestors.
var defaultAncestorStats = TxAncestorStats{}

// TxMiningView represents a snapshot of all transactions, as well as the
// hierarchy of transactions, that are ready to be mined.
type TxMiningView struct {
	rejected           map[chainhash.Hash]struct{}
	txGraph            *txDescGraph
	txDescs            []*TxDesc
	trackAncestorStats bool
	ancestorStats      map[chainhash.Hash]*TxAncestorStats
}

// NewTxMiningView creates a new mining view instance.  The forEachRedeemer
// parameter should define the function to be used for finding transactions that
// spend a given transaction.
func NewTxMiningView(enableAncestorTracking bool,
	forEachRedeemer func(tx *dcrutil.Tx, f func(redeemerTx *TxDesc))) *TxMiningView {

	return &TxMiningView{
		rejected:           make(map[chainhash.Hash]struct{}),
		txGraph:            newTxDescGraph(forEachRedeemer),
		txDescs:            nil,
		trackAncestorStats: enableAncestorTracking,
		ancestorStats:      make(map[chainhash.Hash]*TxAncestorStats),
	}
}

// addAncestorTo modifies the TxAncestorStats instance to include the provided
// TxDesc's statistics.
func addAncestorTo(stats *TxAncestorStats, txDesc *TxDesc) {
	stats.Fees += txDesc.Fee
	stats.SizeBytes += txDesc.TxSize
	stats.TotalSigOps += txDesc.TotalSigOps
	stats.NumAncestors++
}

// addDescendantTo modifies the TxAncestorStats instance to account for the
// addition of a newly tracked descendant.
func addDescendantTo(stats *TxAncestorStats) {
	stats.NumDescendants++
}

// removeAncestorFrom modifies the TxAncestorStats instance to stop storing
// the statistics of the provided TxDesc.
func removeAncestorFrom(stats *TxAncestorStats, txDesc *TxDesc) {
	stats.Fees -= txDesc.Fee
	stats.SizeBytes -= txDesc.TxSize
	stats.TotalSigOps -= txDesc.TotalSigOps
	stats.NumAncestors--
}

// removeDescendantFrom modifies the TxAncestorStats instance to account for
// the removal of a descendant.
func removeDescendantFrom(stats *TxAncestorStats) {
	stats.NumDescendants--
}

// ancestors returns a collection of all transactions in the graph that the
// provided transaction hash depends on, and its ancestors' bundle stats.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) ancestors(txHash *chainhash.Hash) []*TxDesc {
	if !mv.trackAncestorStats {
		return nil
	}

	ancestors := make([]*TxDesc, 0, ancestorTrackingLimit)
	var baseTxStats *TxAncestorStats
	if oldStats, hadStats := mv.ancestorStats[*txHash]; hadStats {
		// If the transaction had statistics tracked already, then we should
		// copy the number of descendants since that value is up to date. Use
		// a new instance of TxAncestorStats to avoid mutating references
		// that may be held between calls to this function.
		baseTxStats = &TxAncestorStats{}
		baseTxStats.NumDescendants = oldStats.NumDescendants
	}

	seen := make(map[chainhash.Hash]struct{}, ancestorTrackingLimit)
	mv.txGraph.forEachAncestor(txHash, seen, func(txDesc *TxDesc) {
		if baseTxStats == nil {
			baseTxStats = &TxAncestorStats{}
		}

		addAncestorTo(baseTxStats, txDesc)
		ancestors = append(ancestors, txDesc)
	})

	if baseTxStats == nil {
		delete(mv.ancestorStats, *txHash)
	} else {
		// Update the cache.
		mv.ancestorStats[*txHash] = baseTxStats
	}

	return ancestors
}

// AncestorStats returns the view's cached statistics for all of the provided
// transaction's ancestors.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) AncestorStats(txHash *chainhash.Hash) (*TxAncestorStats, bool) {
	if !mv.trackAncestorStats {
		return &defaultAncestorStats, false
	}

	if stats, exists := mv.ancestorStats[*txHash]; exists {
		return stats, exists
	}

	return &defaultAncestorStats, false
}

// children returns a set of transactions in the graph that spend from the
// provided transaction hash. The order of elements returned is not guaranteed.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) children(txHash *chainhash.Hash) []*TxDesc {
	childrenOf := mv.txGraph.childrenOf[*txHash]
	if len(childrenOf) == 0 {
		return nil
	}

	children := make([]*TxDesc, 0, len(childrenOf))
	for _, tx := range childrenOf {
		children = append(children, tx)
	}

	return children
}

// Clone makes a deep copy of the mining view and underlying transaction graph.
// fetchTx is the locator functon that is used to find transactions that should
// be included in the mining view.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) Clone(txDescs []*TxDesc, fetchTx TxDescFind) *TxMiningView {
	view := &TxMiningView{
		rejected:           make(map[chainhash.Hash]struct{}),
		txGraph:            mv.txGraph.clone(fetchTx),
		txDescs:            txDescs,
		trackAncestorStats: mv.trackAncestorStats,
		ancestorStats: make(map[chainhash.Hash]*TxAncestorStats,
			len(mv.ancestorStats)),
	}

	for key, value := range mv.ancestorStats {
		view.ancestorStats[key] = &TxAncestorStats{
			Fees:           value.Fees,
			SizeBytes:      value.SizeBytes,
			TotalSigOps:    value.TotalSigOps,
			NumAncestors:   value.NumAncestors,
			NumDescendants: value.NumDescendants,
		}
	}

	return view
}

// descendants returns a collection of transactions in the mining view that
// depend on the provided transaction hash.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) descendants(txHash *chainhash.Hash) []*chainhash.Hash {
	seen := map[chainhash.Hash]struct{}{}
	descendants := make([]*chainhash.Hash, 0)
	mv.txGraph.forEachDescendant(txHash, seen, func(descendant *TxDesc) {
		descendants = append(descendants, descendant.Tx.Hash())
	})
	return descendants
}

// hasParents returns true if the provided transaction hash spends from another
// transaction in the mining view.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) hasParents(txHash *chainhash.Hash) bool {
	return len(mv.txGraph.parentsOf[*txHash]) > 0
}

// isRejected returns true if the given hash has been passed to Reject
// on this instance of the view.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) isRejected(txHash *chainhash.Hash) bool {
	_, exist := mv.rejected[*txHash]
	return exist
}

// parents returns a set of transactions in the graph that the provided
// transaction hash spends from in the view. The order of elements
// returned is not guaranteed.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) parents(txHash *chainhash.Hash) []*TxDesc {
	parentsOf := mv.txGraph.parentsOf[*txHash]
	if len(parentsOf) == 0 {
		return nil
	}

	parents := make([]*TxDesc, 0, len(parentsOf))
	for _, tx := range parentsOf {
		parents = append(parents, tx)
	}

	return parents
}

// maybeUpdateAncestorStats attempts to update the ancestor stats for a given
// transaction. It walks the graph for all ancestors of the provided transaction
// and aggregates statistics for its ancestors. It also accepts a hash for
// an ancestor that should be treated as though it does not exist in the
// graph.
func (mv *TxMiningView) maybeUpdateAncestorStats(txDesc *TxDesc,
	ignoreTxHash *chainhash.Hash) bool {

	baseTxHash := txDesc.Tx.Hash()
	canTrackAncestors := true
	seenAncestors := make(map[chainhash.Hash]*TxDesc,
		ancestorTrackingLimit)
	mv.txGraph.forEachAncestorPreOrder(baseTxHash, seenAncestors,
		func(ancestorTxDesc *TxDesc) bool {
			if !canTrackAncestors {
				// Short circuit the below checks if we know that the provided
				// transaction cannot have ancestor stats aggregated. This also
				// stops adding transactions to the seenAncestors map.
				return false
			}

			ancestorTxHash := *ancestorTxDesc.Tx.Hash()
			if ancestorTxHash == *ignoreTxHash {
				// Skip this ancestor but continue walking others.
				// This accounts for a scenario where a transaction that will be
				// removed from the miningView should cause descendants on the
				// outer edge of the ancestor limit to potentially have ancestor
				// stats calculated. However, in order to walk descendants of
				// the transaction pending removal, it must exist in the graph
				// so ignore it.
				return false
			}

			if len(seenAncestors) >= ancestorTrackingLimit {
				// A transaction with too many ancestors should not have
				// ancestor stats cached.
				canTrackAncestors = false
				return false
			}

			// If any ancestor transaction does not have stats, then the
			// provided one should not either.
			ancestorStats, exists := mv.ancestorStats[ancestorTxHash]
			if !exists {
				canTrackAncestors = false
				return false
			}

			// If any ancestor plus its respective ancestors would exceed the
			// limit for this transaction, then do not track stats.
			if ancestorStats.NumAncestors+1 > ancestorTrackingLimit {
				canTrackAncestors = false
				return false
			}

			// If tracking this transaction would cause any ancestor to have
			// more descendants than the tracking limit, then stats should not
			// be tracked for the base transaction.
			if ancestorStats.NumDescendants+1 > ancestorTrackingLimit {
				canTrackAncestors = false
				return false
			}
			return true
		})

	if !canTrackAncestors {
		delete(mv.ancestorStats, *baseTxHash)
		return false
	}

	// The transaction has passed all checks and will have ancestor statistics
	// tracked and updated. All elements of the seenAncestors map will have
	// ancestor statistics tracked. This is guaranteed by a check performed
	// during a prior walk that limits what enters the seenAncestors map.
	baseTxnAncestorStats := &TxAncestorStats{}
	for ancestorTxHash, ancestorTxDesc := range seenAncestors {
		addAncestorTo(baseTxnAncestorStats, ancestorTxDesc)
		addDescendantTo(mv.ancestorStats[ancestorTxHash])
	}
	mv.ancestorStats[*baseTxHash] = baseTxnAncestorStats
	return true
}

// updateStatsDescendantsAdded adds the provided transaction's stats
// to all transactions in the graph that depend on it.
func (mv *TxMiningView) updateStatsDescendantsAdded(baseTxDesc *TxDesc) {
	if len(mv.txGraph.childrenOf[*baseTxDesc.Tx.Hash()]) == 0 {
		// Return early if the base transaction has no descendants.
		return
	}

	baseTxHash := baseTxDesc.Tx.Hash()
	baseTxStats, baseTxHasStats := mv.ancestorStats[*baseTxHash]
	seenDescendants := make(map[chainhash.Hash]struct{},
		ancestorTrackingLimit+1)

	mv.txGraph.forEachDescendantPreOrder(baseTxHash, seenDescendants,
		func(descendant *TxDesc) bool {
			descendantTxHash := *descendant.Tx.Hash()
			descendantStats, descendantHasStats :=
				mv.ancestorStats[descendantTxHash]
			if !descendantHasStats {
				// Cannot update ancestor stats for a transaction or its
				// descendants if it does not have ancestor stats tracked.
				return false
			}

			if !baseTxHasStats {
				// If the base tx does not have ancestor stats, then remove them
				// for all of its descendants. This can happen during a reorg or
				// disapproval event when two transaction chains are joined by
				// the base transaction.
				delete(mv.ancestorStats, descendantTxHash)
				return true
			}

			if baseTxStats.NumDescendants+1 > ancestorTrackingLimit {
				// The base transaction has enough tracked descendants. Stop
				// tracking this descendant and its descendants.
				delete(mv.ancestorStats, descendantTxHash)
				return true
			}

			if descendantStats.NumAncestors+1 > ancestorTrackingLimit {
				// The descendant has too many tracked ancestors. Stop
				// tracking this descendant and its descendants.
				delete(mv.ancestorStats, descendantTxHash)
				return true
			}

			// Update the stats for this descendant since it is allowed to have
			// ancestor statistics tracked. Also add the descendant to the
			// statistics of the base transaction.
			addAncestorTo(descendantStats, baseTxDesc)
			addDescendantTo(baseTxStats)
			return true
		})
}

// updateStatsDescendantsRemoved removes the provided transaction's stats
// from all descendant transactions.
func (mv *TxMiningView) updateStatsDescendantsRemoved(baseTxDesc *TxDesc) {
	baseTxHash := baseTxDesc.Tx.Hash()
	if _, hasStats := mv.ancestorStats[*baseTxHash]; !hasStats {
		// If the transaction does not have ancestor tracking enabled, then
		// none of its descendants should either.
		return
	}

	numUntrackedDescendants := 0
	seenDescendants := make(map[chainhash.Hash]struct{}, ancestorTrackingLimit)
	mv.txGraph.forEachDescendantPreOrder(baseTxHash, seenDescendants,
		func(descendantTxDesc *TxDesc) bool {
			descendantTxHash := *descendantTxDesc.Tx.Hash()
			descendantStats, hasStats := mv.ancestorStats[descendantTxHash]

			// Attempt to track the descendant's ancestor stats if it was not
			// tracked previously. This scenario applies to a transaction
			// that once had too many ancestors, but is now eligible to
			// have stats tracked.
			// A check is also performed to avoid testing too many descendants
			// for eligibility to be included in the tracked set, since no
			// descendant can know if any one of its ancestors has too many
			// descendants tracked without visiting the ancestor. Note that
			// although this has a non-ideal behavior of potentially skipping
			// transactions that would otherwise be eligible to have stats
			// tracked, it bounds the number of times ancestors are walked.
			if !hasStats && numUntrackedDescendants < ancestorTrackingLimit {
				numUntrackedDescendants++

				// Note that this call retrieves ancestors while walking
				// descendants of another transaction, and that the time
				// complexity is bound by the maximum number of ancestors
				// allowed to be tracked in the graph and the maximum
				// number of descendants allowed to have ancestor stats tracked.
				mv.maybeUpdateAncestorStats(descendantTxDesc, baseTxHash)

				// Do not walk descendants of this descendant if it did not
				// have ancestor stats previously because it was on the edge of
				// the ancestor limit. Any transactions that descend from it
				// would exceed the limit by at least one.
				return false
			}

			if hasStats {
				removeAncestorFrom(descendantStats, baseTxDesc)
				return true
			}

			// If the transaction does not have statistics tracked then none of
			// its descendants will either.
			return false
		})

	// Update all ancestors to account for the removal of a descendant.
	seenAncestors := make(map[chainhash.Hash]*TxDesc, ancestorTrackingLimit)
	mv.txGraph.forEachAncestorPreOrder(baseTxHash, seenAncestors,
		func(ancestorTxDesc *TxDesc) bool {
			ancestorStats, exists := mv.ancestorStats[*ancestorTxDesc.Tx.Hash()]
			if exists {
				removeDescendantFrom(ancestorStats)
			}
			return true
		})
}

// AddTransaction inserts a TxDesc into the mining view if it has a parent
// or child in the view, or has a relationship with another transaction through
// findTx.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) AddTransaction(txDesc *TxDesc, findTx TxDescFind) {
	// Track the txn in the graph and attempt to relate it to at least one other
	// transaction.
	mv.txGraph.insert(txDesc, findTx)

	if mv.trackAncestorStats {
		mv.maybeUpdateAncestorStats(txDesc, &zeroHash)
		// When a transaction is added back to the view, update the stats of its
		// descendants.
		mv.updateStatsDescendantsAdded(txDesc)
	}
}

// RemoveTransaction stops tracking the transaction in the mining view if it
// exists.  If updateDescendantStats is true, then the statistics for all
// descendant transactions are updated to account for the removal of an
// ancestor.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) RemoveTransaction(txHash *chainhash.Hash, updateDescendantStats bool) {
	if mv.trackAncestorStats && updateDescendantStats {
		txDesc := mv.txGraph.find(txHash)
		if txDesc != nil {
			mv.updateStatsDescendantsRemoved(txDesc)
		}
	}

	mv.txGraph.remove(txHash)
	delete(mv.ancestorStats, *txHash)
}

// reject stops tracking the transaction in the view, if it exists, and all
// of its descendants.  Also flags the provided transaction as rejected and
// tracks the hash as rejected in this instance of the mining view. Rejected
// transactions are not shared between mining view instances, and a new
// mining view is always initialized with an empty set of rejected transactions.
//
// This function is NOT safe for concurrent access.
func (mv *TxMiningView) reject(txHash *chainhash.Hash) {
	seen := make(map[chainhash.Hash]struct{})
	mv.txGraph.forEachDescendant(txHash, seen, func(descendant *TxDesc) {
		mv.RemoveTransaction(descendant.Tx.Hash(), false)
		mv.rejected[*descendant.Tx.Hash()] = struct{}{}
	})

	mv.RemoveTransaction(txHash, false)
	mv.rejected[*txHash] = struct{}{}
}

// TxDescs returns a collection of all transactions available in the view.
func (mv *TxMiningView) TxDescs() []*TxDesc {
	return mv.txDescs
}
