package graphdb

import (
	"time"

	"github.com/lightningnetwork/lnd/lnwire"
)

// rejectFlags is a compact representation of various metadata stored by the
// reject cache about a particular channel.
type rejectFlags uint8

const (
	// rejectFlagExists is a flag indicating whether the channel exists,
	// i.e. the channel is open and has a recent channel update. If this
	// flag is not set, the channel is either a zombie or unknown.
	rejectFlagExists rejectFlags = 1 << iota

	// rejectFlagZombie is a flag indicating whether the channel is a
	// zombie, i.e. the channel is open but has no recent channel updates.
	rejectFlagZombie
)

// packRejectFlags computes the rejectFlags corresponding to the passed boolean
// values indicating whether the edge exists or is a zombie.
func packRejectFlags(exists, isZombie bool) rejectFlags {
	var flags rejectFlags
	if exists {
		flags |= rejectFlagExists
	}
	if isZombie {
		flags |= rejectFlagZombie
	}

	return flags
}

// unpack returns the booleans packed into the rejectFlags. The first indicates
// if the edge exists in our graph, the second indicates if the edge is a
// zombie.
func (f rejectFlags) unpack() (bool, bool) {
	return f&rejectFlagExists == rejectFlagExists,
		f&rejectFlagZombie == rejectFlagZombie
}

// rejectCacheEntry caches frequently accessed information about a channel,
// including the timestamps of its latest edge policies and whether or not the
// channel exists in the graph.
type rejectCacheEntry struct {
	// upd{1,2}Time are Unix timestamps for v1 policies.
	upd1Time int64
	upd2Time int64

	// upd{1,2}BlockHeight are the last known block heights for v2
	// policies.
	upd1BlockHeight uint32
	upd2BlockHeight uint32

	flags rejectFlags
}

func newRejectCacheEntryV1(upd1, upd2 time.Time, exists,
	isZombie bool) rejectCacheEntry {

	return rejectCacheEntry{
		upd1Time: upd1.Unix(),
		upd2Time: upd2.Unix(),
		flags:    packRejectFlags(exists, isZombie),
	}
}

func newRejectCacheEntryV2(upd1, upd2 uint32, exists,
	isZombie bool) rejectCacheEntry {

	return rejectCacheEntry{
		upd1BlockHeight: upd1,
		upd2BlockHeight: upd2,
		flags:           packRejectFlags(exists, isZombie),
	}
}

func updateRejectCacheEntryV1(entry *rejectCacheEntry, isUpdate1 bool,
	lastUpdate time.Time) {

	if isUpdate1 {
		entry.upd1Time = lastUpdate.Unix()
	} else {
		entry.upd2Time = lastUpdate.Unix()
	}
}

func updateRejectCacheEntryV2(entry *rejectCacheEntry, isUpdate1 bool,
	blockHeight uint32) {

	if isUpdate1 {
		entry.upd1BlockHeight = blockHeight
	} else {
		entry.upd2BlockHeight = blockHeight
	}
}

// rejectCacheKey uniquely identifies a channel entry in the reject cache by
// gossip version and channel ID.
type rejectCacheKey struct {
	version lnwire.GossipVersion
	chanID  uint64
}

// rejectCache is an in-memory cache used to improve the performance of
// HasChannelEdge. It caches information about the whether or channel exists, as
// well as the most recent timestamps for each policy (if they exists).
type rejectCache struct {
	n     int
	edges map[rejectCacheKey]rejectCacheEntry
}

// newRejectCache creates a new rejectCache with maximum capacity of n entries.
func newRejectCache(n int) *rejectCache {
	return &rejectCache{
		n:     n,
		edges: make(map[rejectCacheKey]rejectCacheEntry, n),
	}
}

// get returns the entry from the cache for chanid, if it exists.
func (c *rejectCache) get(version lnwire.GossipVersion, chanid uint64) (
	rejectCacheEntry, bool) {

	entry, ok := c.edges[rejectCacheKey{
		version: version,
		chanID:  chanid,
	}]
	return entry, ok
}

// insert adds the entry to the reject cache. If an entry for chanid already
// exists, it will be replaced with the new entry. If the entry doesn't exists,
// it will be inserted to the cache, performing a random eviction if the cache
// is at capacity.
func (c *rejectCache) insert(version lnwire.GossipVersion, chanid uint64,
	entry rejectCacheEntry) {

	key := rejectCacheKey{
		version: version,
		chanID:  chanid,
	}

	// If entry exists, replace it.
	if _, ok := c.edges[key]; ok {
		c.edges[key] = entry
		return
	}

	// Otherwise, evict an entry at random and insert.
	if len(c.edges) == c.n {
		for id := range c.edges {
			delete(c.edges, id)
			break
		}
	}
	c.edges[key] = entry
}

// remove deletes an entry for chanid from the cache, if it exists.
func (c *rejectCache) remove(version lnwire.GossipVersion, chanid uint64) {
	delete(c.edges, rejectCacheKey{
		version: version,
		chanID:  chanid,
	})
}
