package protocol

import (
	"sync"
	"time"

	log "github.com/sirupsen/logrus"

	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
)

var (
	orphanBlockTTL           = 3 * time.Minute
	orphanExpireWorkInterval = 1 * time.Minute
	numOrphanBlockLimit      = 256
)

type orphanBlock struct {
	*types.Block
	expiration time.Time
}

// OrphanManage is use to handle all the orphan block
type OrphanManage struct {
	orphan      map[bc.Hash]*orphanBlock
	prevOrphans map[bc.Hash][]*bc.Hash
	mtx         sync.RWMutex
}

// NewOrphanManage return a new orphan block
func NewOrphanManage() *OrphanManage {
	o := &OrphanManage{
		orphan:      make(map[bc.Hash]*orphanBlock),
		prevOrphans: make(map[bc.Hash][]*bc.Hash),
	}

	go o.orphanExpireWorker()
	return o
}

// BlockExist check is the block in OrphanManage
func (o *OrphanManage) BlockExist(hash *bc.Hash) bool {
	o.mtx.RLock()
	_, ok := o.orphan[*hash]
	o.mtx.RUnlock()
	return ok
}

// Add will add the block to OrphanManage
func (o *OrphanManage) Add(block *types.Block) {
	blockHash := block.Hash()
	o.mtx.Lock()
	defer o.mtx.Unlock()

	if _, ok := o.orphan[blockHash]; ok {
		return
	}

	if len(o.orphan) >= numOrphanBlockLimit {
		o.deleteLRU()
		log.WithFields(log.Fields{"module": logModule, "hash": blockHash.String(), "height": block.Height}).Info("the number of orphan blocks exceeds the limit")
	}

	o.orphan[blockHash] = &orphanBlock{block, time.Now().Add(orphanBlockTTL)}
	o.prevOrphans[block.PreviousBlockHash] = append(o.prevOrphans[block.PreviousBlockHash], &blockHash)

	log.WithFields(log.Fields{"module": logModule, "hash": blockHash.String(), "height": block.Height}).Info("add block to orphan")
}

// Delete will delete the block from OrphanManage
func (o *OrphanManage) Delete(hash *bc.Hash) {
	o.mtx.Lock()
	defer o.mtx.Unlock()
	o.delete(hash)
}

// Get return the orphan block by hash
func (o *OrphanManage) Get(hash *bc.Hash) (*types.Block, bool) {
	o.mtx.RLock()
	block, ok := o.orphan[*hash]
	o.mtx.RUnlock()
	return block.Block, ok
}

// GetPrevOrphans return the list of child orphans
func (o *OrphanManage) GetPrevOrphans(hash *bc.Hash) ([]*bc.Hash, bool) {
	o.mtx.RLock()
	prevOrphans, ok := o.prevOrphans[*hash]
	o.mtx.RUnlock()
	return prevOrphans, ok
}

func (o *OrphanManage) delete(hash *bc.Hash) {
	block, ok := o.orphan[*hash]
	if !ok {
		return
	}
	delete(o.orphan, *hash)

	prevOrphans, ok := o.prevOrphans[block.Block.PreviousBlockHash]
	if !ok || len(prevOrphans) == 1 {
		delete(o.prevOrphans, block.Block.PreviousBlockHash)
		return
	}

	for i, preOrphan := range prevOrphans {
		if *preOrphan == *hash {
			o.prevOrphans[block.Block.PreviousBlockHash] = append(prevOrphans[:i], prevOrphans[i+1:]...)
			return
		}
	}
}

func (o *OrphanManage) deleteLRU() {
	var deleteBlock *orphanBlock
	for _, orphan := range o.orphan {
		if deleteBlock == nil || orphan.expiration.Before(deleteBlock.expiration) {
			deleteBlock = orphan
		}
	}

	if deleteBlock != nil {
		blockHash := deleteBlock.Block.Hash()
		o.delete(&blockHash)
	}
}

func (o *OrphanManage) orphanExpireWorker() {
	ticker := time.NewTicker(orphanExpireWorkInterval)
	for now := range ticker.C {
		o.orphanExpire(now)
	}
	ticker.Stop()
}

func (o *OrphanManage) orphanExpire(now time.Time) {
	o.mtx.Lock()
	defer o.mtx.Unlock()
	for hash, orphan := range o.orphan {
		if orphan.expiration.Before(now) {
			o.delete(&hash)
		}
	}
}
