package core

import (
	"sort"
	"sync"
	"time"

	"github.com/CityOfZion/neo-go/pkg/core/transaction"
	"github.com/CityOfZion/neo-go/pkg/util"
)

// PoolItem represents a transaction in the the Memory pool.
type PoolItem struct {
	txn       *transaction.Transaction
	timeStamp time.Time
	fee       Feer
}

// PoolItems slice of PoolItem
type PoolItems []*PoolItem

// MemPool stores the unconfirms transactions.
type MemPool struct {
	lock                        *sync.RWMutex
	unsortedTxn                 map[util.Uint256]*PoolItem
	unverifiedTxn               map[util.Uint256]*PoolItem
	sortedHighPrioTxn           PoolItems
	sortedLowPrioTxn            PoolItems
	unverifiedSortedHighPrioTxn PoolItems
	unverifiedSortedLowPrioTxn  PoolItems

	capacity int
}

func (p PoolItems) Len() int           { return len(p) }
func (p PoolItems) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p PoolItems) Less(i, j int) bool { return p[i].CompareTo(p[j]) < 0 }

// CompareTo returns the difference between two PoolItems.
// difference < 0 implies p < otherP.
// difference = 0 implies p = otherP.
// difference > 0 implies p > otherP.
func (p PoolItem) CompareTo(otherP *PoolItem) int {
	if otherP == nil {
		return 1
	}

	if p.fee.IsLowPriority(p.txn) && p.fee.IsLowPriority(otherP.txn) {
		thisIsClaimTx := p.txn.Type == transaction.ClaimType
		otherIsClaimTx := otherP.txn.Type == transaction.ClaimType

		if thisIsClaimTx != otherIsClaimTx {
			// This is a claim Tx and other isn't.
			if thisIsClaimTx {
				return 1
			}
			// The other is claim Tx and this isn't.
			return -1
		}
	}

	// Fees sorted ascending
	pFPB := p.fee.FeePerByte(p.txn)
	otherFPB := p.fee.FeePerByte(otherP.txn)
	if ret := pFPB.CompareTo(otherFPB); ret != 0 {
		return ret
	}

	pNF := p.fee.NetworkFee(p.txn)
	otherNF := p.fee.NetworkFee(otherP.txn)
	if ret := pNF.CompareTo(otherNF); ret != 0 {
		return ret
	}

	// Transaction hash sorted descending
	return otherP.txn.Hash().CompareTo(p.txn.Hash())
}

// Count returns the total number of uncofirm transactions.
func (mp MemPool) Count() int {
	mp.lock.RLock()
	defer mp.lock.RUnlock()

	return len(mp.unsortedTxn) + len(mp.unverifiedTxn)
}

// ContainsKey checks if a transactions hash is in the MemPool.
func (mp MemPool) ContainsKey(hash util.Uint256) bool {
	mp.lock.RLock()
	defer mp.lock.RUnlock()

	if _, ok := mp.unsortedTxn[hash]; ok {
		return true
	}

	if _, ok := mp.unverifiedTxn[hash]; ok {
		return true
	}

	return false
}

// TryAdd try to add the PoolItem to the MemPool.
func (mp MemPool) TryAdd(hash util.Uint256, pItem *PoolItem) bool {
	var pool PoolItems

	mp.lock.RLock()
	if _, ok := mp.unsortedTxn[hash]; ok {
		return false
	}
	mp.unsortedTxn[hash] = pItem
	mp.lock.RUnlock()

	if pItem.fee.IsLowPriority(pItem.txn) {
		pool = mp.sortedLowPrioTxn
	} else {
		pool = mp.sortedHighPrioTxn
	}

	mp.lock.Lock()
	pool = append(pool, pItem)
	sort.Sort(pool)
	mp.lock.Unlock()

	if mp.Count() > mp.capacity {
		(&mp).RemoveOverCapacity()
	}
	mp.lock.RLock()
	_, ok := mp.unsortedTxn[hash]
	mp.lock.RUnlock()
	return ok
}

// RemoveOverCapacity removes transactions with lowest fees until the total number of transactions
// in the MemPool is within the capacity of the MemPool.
func (mp *MemPool) RemoveOverCapacity() {
	for mp.Count()-mp.capacity > 0 {
		mp.lock.Lock()
		if minItem, argPosition := getLowestFeeTransaction(mp.sortedLowPrioTxn, mp.unverifiedSortedLowPrioTxn); minItem != nil {
			if argPosition == 1 {
				// minItem belongs to the mp.sortedLowPrioTxn slice.
				// The corresponding unsorted pool is is mp.unsortedTxn.
				delete(mp.unsortedTxn, minItem.txn.Hash())
				mp.sortedLowPrioTxn = append(mp.sortedLowPrioTxn[:0], mp.sortedLowPrioTxn[1:]...)
			} else {
				// minItem belongs to the mp.unverifiedSortedLowPrioTxn slice.
				// The corresponding unsorted pool is is mp.unverifiedTxn.
				delete(mp.unverifiedTxn, minItem.txn.Hash())
				mp.unverifiedSortedLowPrioTxn = append(mp.unverifiedSortedLowPrioTxn[:0], mp.unverifiedSortedLowPrioTxn[1:]...)

			}
		} else if minItem, argPosition := getLowestFeeTransaction(mp.sortedHighPrioTxn, mp.unverifiedSortedHighPrioTxn); minItem != nil {
			if argPosition == 1 {
				// minItem belongs to the mp.sortedHighPrioTxn slice.
				// The corresponding unsorted pool is is mp.unsortedTxn.
				delete(mp.unsortedTxn, minItem.txn.Hash())
				mp.sortedHighPrioTxn = append(mp.sortedHighPrioTxn[:0], mp.sortedHighPrioTxn[1:]...)
			} else {
				// minItem belongs to the mp.unverifiedSortedHighPrioTxn slice.
				// The corresponding unsorted pool is is mp.unverifiedTxn.
				delete(mp.unverifiedTxn, minItem.txn.Hash())
				mp.unverifiedSortedHighPrioTxn = append(mp.unverifiedSortedHighPrioTxn[:0], mp.unverifiedSortedHighPrioTxn[1:]...)

			}
		}
		mp.lock.Unlock()
	}

}

// NewPoolItem returns a new PoolItem.
func NewPoolItem(t *transaction.Transaction, fee Feer) *PoolItem {
	return &PoolItem{
		txn:       t,
		timeStamp: time.Now().UTC(),
		fee:       fee,
	}
}

// NewMemPool returns a new MemPool struct.
func NewMemPool(capacity int) MemPool {
	return MemPool{
		lock:          new(sync.RWMutex),
		unsortedTxn:   make(map[util.Uint256]*PoolItem),
		unverifiedTxn: make(map[util.Uint256]*PoolItem),
		capacity:      capacity,
	}
}

// TryGetValue returns a transactions if it esists in the memory pool.
func (mp MemPool) TryGetValue(hash util.Uint256) (*transaction.Transaction, bool) {
	mp.lock.Lock()
	defer mp.lock.Unlock()
	if pItem, ok := mp.unsortedTxn[hash]; ok {
		return pItem.txn, ok
	}

	if pItem, ok := mp.unverifiedTxn[hash]; ok {
		return pItem.txn, ok
	}

	return nil, false
}

// getLowestFeeTransaction returns the PoolItem with the lowest fee amongst the "verifiedTxnSorted"
// and "unverifiedTxnSorted" PoolItems along with a integer. The integer can assume two values, 1 and 2 which indicate
// that the PoolItem with the lowest fee was found in "verifiedTxnSorted" respectively in "unverifiedTxnSorted".
// "verifiedTxnSorted" and "unverifiedTxnSorted" are sorted slice order by transaction fee ascending. This means that
// the transaction with lowest fee start at index 0.
// Reference: GetLowestFeeTransaction method in C# (https://github.com/neo-project/neo/blob/master/neo/Ledger/MemoryPool.cs)
func getLowestFeeTransaction(verifiedTxnSorted PoolItems, unverifiedTxnSorted PoolItems) (*PoolItem, int) {
	minItem := min(unverifiedTxnSorted)
	verifiedMin := min(verifiedTxnSorted)
	if verifiedMin == nil || (minItem != nil && verifiedMin.CompareTo(minItem) >= 0) {
		return minItem, 2
	}

	minItem = verifiedMin
	return minItem, 1

}

// min return the minimum item in a ascending sorted slice of pool items.
// The  function can't be applied to unsorted slice!
func min(sortedPool PoolItems) *PoolItem {
	if len(sortedPool) == 0 {
		return nil
	}
	return sortedPool[0]
}

// GetVerifiedTransactions returns a slice of Input from all the transactions in the memory pool
// whose hash is not included in excludedHashes.
func (mp *MemPool) GetVerifiedTransactions() []*transaction.Transaction {
	var t []*transaction.Transaction

	mp.lock.Lock()
	defer mp.lock.Unlock()
	for _, p := range mp.unsortedTxn {
		t = append(t, p.txn)
	}

	return t
}

// Verify verifies if the inputs of a transaction tx are already used in any other transaction in the memory pool.
// If yes, the transaction tx is not a valid transaction and the function return false.
// If no, the transaction tx is a valid transaction and the function return true.
func (mp MemPool) Verify(tx *transaction.Transaction) bool {
	count := 0
	inputs := make([]*transaction.Input, 0)
	for _, item := range mp.GetVerifiedTransactions() {
		if tx.Hash().Equals(item.Hash()) {
			inputs = append(inputs, item.Inputs...)
		}
	}

	for i := 0; i < len(inputs); i++ {
		for j := 0; j < len(tx.Inputs); j++ {
			if inputs[i].PrevHash.Equals(tx.Inputs[j].PrevHash) {
				count++
			}
		}
	}

	return count == 0
}