/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package discovery

import (
	"math/rand"
	"sort"
	"time"

	"github.com/hyperledger/fabric/gossip/protoext"
)

// Filter filters and sorts the given endorsers
type Filter interface {
	Filter(endorsers Endorsers) Endorsers
}

// ExclusionFilter returns true if the given Peer
// is not to be considered when selecting peers
type ExclusionFilter interface {
	// Exclude returns whether the given Peer is to be excluded or not
	Exclude(Peer) bool
}

type selectionFunc func(Peer) bool

func (sf selectionFunc) Exclude(p Peer) bool {
	return sf(p)
}

// PrioritySelector guides the selection of peers via
// giving peers a relative priority to their selection
type PrioritySelector interface {
	// Compare compares between 2 peers and returns
	// their relative scores
	Compare(Peer, Peer) Priority
}

// Priority defines how likely a peer is to be selected
// over another peer.
// Positive priority means the left peer is selected
// Negative priority means the right peer is selected
// Zero priority means their priorities are the same
type Priority int

var (
	// PrioritiesByHeight selects peers by descending height
	PrioritiesByHeight = &byHeight{}
	// NoExclusion accepts all peers and rejects no peers
	NoExclusion = selectionFunc(noExclusion)
	// NoPriorities is indifferent to how it selects peers
	NoPriorities = &noPriorities{}
)

type noPriorities struct{}

func (nc noPriorities) Compare(_ Peer, _ Peer) Priority {
	return 0
}

type byHeight struct{}

func (*byHeight) Compare(left Peer, right Peer) Priority {
	leftHeight := left.StateInfoMessage.GetStateInfo().Properties.LedgerHeight
	rightHeight := right.StateInfoMessage.GetStateInfo().Properties.LedgerHeight

	if leftHeight > rightHeight {
		return 1
	}
	if rightHeight > leftHeight {
		return -1
	}
	return 0
}

func noExclusion(_ Peer) bool {
	return false
}

// ExcludeHosts returns a ExclusionFilter that excludes the given endpoints
func ExcludeHosts(endpoints ...string) ExclusionFilter {
	m := make(map[string]struct{})
	for _, endpoint := range endpoints {
		m[endpoint] = struct{}{}
	}
	return ExcludeByHost(func(host string) bool {
		_, excluded := m[host]
		return excluded
	})
}

// ExcludeByHost creates a ExclusionFilter out of the given exclusion predicate
func ExcludeByHost(reject func(host string) bool) ExclusionFilter {
	return selectionFunc(func(p Peer) bool {
		endpoint := p.AliveMessage.GetAliveMsg().Membership.Endpoint
		var internalEndpoint string
		se := p.AliveMessage.GetSecretEnvelope()
		if se != nil {
			internalEndpoint = protoext.InternalEndpoint(se)
		}
		return reject(endpoint) || reject(internalEndpoint)
	})
}

// Filter filters the endorsers according to the given ExclusionFilter
func (endorsers Endorsers) Filter(f ExclusionFilter) Endorsers {
	var res Endorsers
	for _, e := range endorsers {
		if !f.Exclude(*e) {
			res = append(res, e)
		}
	}
	return res
}

// Shuffle sorts the endorsers in random order
func (endorsers Endorsers) Shuffle() Endorsers {
	res := make(Endorsers, len(endorsers))
	rand.Seed(time.Now().UnixNano())
	for i, index := range rand.Perm(len(endorsers)) {
		res[i] = endorsers[index]
	}
	return res
}

type endorserSort struct {
	Endorsers
	PrioritySelector
}

// Sort sorts the endorsers according to the given PrioritySelector
func (endorsers Endorsers) Sort(ps PrioritySelector) Endorsers {
	sort.Sort(&endorserSort{
		Endorsers:        endorsers,
		PrioritySelector: ps,
	})
	return endorsers
}

func (es *endorserSort) Len() int {
	return len(es.Endorsers)
}

func (es *endorserSort) Less(i, j int) bool {
	e1 := es.Endorsers[i]
	e2 := es.Endorsers[j]
	less := es.Compare(*e1, *e2)
	return less > Priority(0)
}

func (es *endorserSort) Swap(i, j int) {
	es.Endorsers[i], es.Endorsers[j] = es.Endorsers[j], es.Endorsers[i]
}
