package backoff

import (
	
	
	
	

	
	

	ma 
)

// BackoffDiscovery is an implementation of discovery that caches peer data and attenuates repeated queries
type BackoffDiscovery struct {
	disc         discovery.Discovery
	stratFactory BackoffFactory
	peerCache    map[string]*backoffCache
	peerCacheMux sync.RWMutex

	parallelBufSz int
	returnedBufSz int

	clock clock
}

type BackoffDiscoveryOption func(*BackoffDiscovery) error

func ( discovery.Discovery,  BackoffFactory,  ...BackoffDiscoveryOption) (discovery.Discovery, error) {
	 := &BackoffDiscovery{
		disc:         ,
		stratFactory: ,
		peerCache:    make(map[string]*backoffCache),

		parallelBufSz: 32,
		returnedBufSz: 32,

		clock: realClock{},
	}

	for ,  := range  {
		if  := ();  != nil {
			return nil, 
		}
	}

	return , nil
}

// WithBackoffDiscoverySimultaneousQueryBufferSize sets the buffer size for the channels between the main FindPeers query
// for a given namespace and all simultaneous FindPeers queries for the namespace
func ( int) BackoffDiscoveryOption {
	return func( *BackoffDiscovery) error {
		if  < 0 {
			return fmt.Errorf("cannot set size to be smaller than 0")
		}
		.parallelBufSz = 
		return nil
	}
}

// WithBackoffDiscoveryReturnedChannelSize sets the size of the buffer to be used during a FindPeer query.
// Note: This does not apply if the query occurs during the backoff time
func ( int) BackoffDiscoveryOption {
	return func( *BackoffDiscovery) error {
		if  < 0 {
			return fmt.Errorf("cannot set size to be smaller than 0")
		}
		.returnedBufSz = 
		return nil
	}
}

type clock interface {
	Now() time.Time
}

type realClock struct{}

func ( realClock) () time.Time {
	return time.Now()
}

type backoffCache struct {
	// strat is assigned on creation and not written to
	strat BackoffStrategy

	mux          sync.Mutex // guards writes to all following fields
	nextDiscover time.Time
	prevPeers    map[peer.ID]peer.AddrInfo
	peers        map[peer.ID]peer.AddrInfo
	sendingChs   map[chan peer.AddrInfo]int
	ongoing      bool

	clock clock
}

func ( *BackoffDiscovery) ( context.Context,  string,  ...discovery.Option) (time.Duration, error) {
	return .disc.Advertise(, , ...)
}

func ( *BackoffDiscovery) ( context.Context,  string,  ...discovery.Option) (<-chan peer.AddrInfo, error) {
	// Get options
	var  discovery.Options
	 := .Apply(...)
	if  != nil {
		return nil, 
	}

	// Get cached peers
	.peerCacheMux.RLock()
	,  := .peerCache[]
	.peerCacheMux.RUnlock()

	/*
		Overall plan:
		If it's time to look for peers, look for peers, then return them
		If it's not time then return cache
		If it's time to look for peers, but we have already started looking. Get up to speed with ongoing request
	*/

	// Setup cache if we don't have one yet
	if ! {
		 := &backoffCache{
			nextDiscover: time.Time{},
			prevPeers:    make(map[peer.ID]peer.AddrInfo),
			peers:        make(map[peer.ID]peer.AddrInfo),
			sendingChs:   make(map[chan peer.AddrInfo]int),
			strat:        .stratFactory(),
			clock:        .clock,
		}

		.peerCacheMux.Lock()
		,  = .peerCache[]

		if ! {
			.peerCache[] = 
			 = 
		}

		.peerCacheMux.Unlock()
	}

	.mux.Lock()
	defer .mux.Unlock()

	 := .clock.Now().After(.nextDiscover)

	// If it's not yet time to search again and no searches are in progress then return cached peers
	if !( || .ongoing) {
		 := .Limit

		if  == 0 {
			 = len(.prevPeers)
		} else if  > len(.prevPeers) {
			 = len(.prevPeers)
		}
		 := make(chan peer.AddrInfo, )
		for ,  := range .prevPeers {
			select {
			case  <- :
			default:
				// skip if we have asked for a lower limit than the number of peers known
			}
		}
		close()
		return , nil
	}

	// If a request is not already in progress setup a dispatcher channel for dispatching incoming peers
	if !.ongoing {
		,  := .disc.FindPeers(, , ...)
		if  != nil {
			return nil, 
		}

		.ongoing = true
		go findPeerDispatcher(, , )
	}

	// Setup receiver channel for receiving peers from ongoing requests
	 := make(chan peer.AddrInfo, .parallelBufSz)
	 := make(chan peer.AddrInfo, .returnedBufSz)
	 := make([]peer.AddrInfo, 0, 32)
	for ,  := range .peers {
		 = append(, )
	}
	.sendingChs[] = .Limit

	go findPeerReceiver(, , , )

	return , nil
}

func findPeerDispatcher( context.Context,  *backoffCache,  <-chan peer.AddrInfo) {
	defer func() {
		.mux.Lock()

		// If the peer addresses have changed reset the backoff
		if checkUpdates(.prevPeers, .peers) {
			.strat.Reset()
			.prevPeers = .peers
		}
		.nextDiscover = .clock.Now().Add(.strat.Delay())

		.ongoing = false
		.peers = make(map[peer.ID]peer.AddrInfo)

		for  := range .sendingChs {
			close()
		}
		.sendingChs = make(map[chan peer.AddrInfo]int)
		.mux.Unlock()
	}()

	for {
		select {
		case ,  := <-:
			if ! {
				return
			}
			.mux.Lock()

			// If we receive the same peer multiple times return the address union
			var  peer.AddrInfo
			if ,  := .peers[.ID];  {
				if  := mergeAddrInfos(, );  != nil {
					 = *
				} else {
					.mux.Unlock()
					continue
				}
			} else {
				 = 
			}

			.peers[.ID] = 

			for ,  := range .sendingChs {
				if  > 0 {
					 <- 
					.sendingChs[] =  - 1
				}
			}

			.mux.Unlock()
		case <-.Done():
			return
		}
	}
}

func findPeerReceiver( context.Context, ,  chan peer.AddrInfo,  []peer.AddrInfo) {
	defer close()

	for {
		select {
		case ,  := <-:
			if  {
				 = append(, )

				 := true
			:
				for ,  := range  {
					select {
					case  <- :
					default:
						 = [:]
						 = false
						break 
					}
				}
				if  {
					 = []peer.AddrInfo{}
				}
			} else {
				for ,  := range  {
					select {
					case  <- :
					case <-.Done():
						return
					}
				}
				return
			}
		case <-.Done():
			return
		}
	}
}

func mergeAddrInfos(,  peer.AddrInfo) *peer.AddrInfo {
	 := make(map[string]struct{}, len(.Addrs))
	 := make([]ma.Multiaddr, 0, len(.Addrs))
	 := func( []ma.Multiaddr) {
		for ,  := range  {
			if ,  := [.String()];  {
				continue
			}
			[.String()] = struct{}{}
			 = append(, )
		}
	}
	(.Addrs)
	(.Addrs)

	if len() > len(.Addrs) {
		 := &peer.AddrInfo{ID: .ID, Addrs: }
		return 
	}
	return nil
}

func checkUpdates(,  map[peer.ID]peer.AddrInfo) bool {
	if len() != len() {
		return true
	}
	for ,  := range  {
		if ,  := [];  {
			if  := mergeAddrInfos(, );  != nil {
				return true
			}
		} else {
			return true
		}
	}
	return false
}