package autopilot

import (
	"fmt"
	"sync"
)

// stack is a simple int stack to help with readability of Brandes'
// betweenness centrality implementation below.
type stack struct {
	stack []int
}

func (s *stack) push(v int) {
	s.stack = append(s.stack, v)
}

func (s *stack) top() int {
	return s.stack[len(s.stack)-1]
}

func (s *stack) pop() {
	s.stack = s.stack[:len(s.stack)-1]
}

func (s *stack) empty() bool {
	return len(s.stack) == 0
}

// queue is a simple int queue to help with readability of Brandes'
// betweenness centrality implementation below.
type queue struct {
	queue []int
}

func (q *queue) push(v int) {
	q.queue = append(q.queue, v)
}

func (q *queue) front() int {
	return q.queue[0]
}

func (q *queue) pop() {
	q.queue = q.queue[1:]
}

func (q *queue) empty() bool {
	return len(q.queue) == 0
}

// BetweennessCentrality is a NodeMetric that calculates node betweenness
// centrality using Brandes' algorithm. Betweenness centrality for each node
// is the number of shortest paths passing through that node, not counting
// shortest paths starting or ending at that node. This is a useful metric
// to measure control of individual nodes over the whole network.
type BetweennessCentrality struct {
	// workers number of goroutines are used to parallelize
	// centrality calculation.
	workers int

	// centrality stores original (not normalized) centrality values for
	// each node in the graph.
	centrality map[NodeID]float64

	// min is the minimum centrality in the graph.
	min float64

	// max is the maximum centrality in the graph.
	max float64
}

// NewBetweennessCentralityMetric creates a new BetweennessCentrality instance.
// Users can specify the number of workers to use for calculating centrality.
func NewBetweennessCentralityMetric(workers int) (*BetweennessCentrality, error) {
	// There should be at least one worker.
	if workers < 1 {
		return nil, fmt.Errorf("workers must be positive")
	}
	return &BetweennessCentrality{
		workers: workers,
	}, nil
}

// Name returns the name of the metric.
func (bc *BetweennessCentrality) Name() string {
	return "betweenness_centrality"
}

// betweennessCentrality is the core of Brandes' algorithm.
// We first calculate the shortest paths from the start node s to all other
// nodes with BFS, then update the betweenness centrality values by using
// Brandes' dependency trick.
// For detailed explanation please read:
// https://www.cl.cam.ac.uk/teaching/1617/MLRD/handbook/brandes.html
func betweennessCentrality(g *SimpleGraph, s int, centrality []float64) {
	// pred[w] is the list of nodes that immediately precede w on a
	// shortest path from s to t for each node t.
	pred := make([][]int, len(g.Nodes))

	// sigma[t] is the number of shortest paths between nodes s and t
	// for each node t.
	sigma := make([]int, len(g.Nodes))
	sigma[s] = 1

	// dist[t] holds the distance between s and t for each node t.
	// We initialize this to -1 (meaning infinity) for each t != s.
	dist := make([]int, len(g.Nodes))
	for i := range dist {
		dist[i] = -1
	}

	dist[s] = 0

	var (
		st stack
		q  queue
	)
	q.push(s)

	// BFS to calculate the shortest paths (sigma and pred)
	// from s to t for each node t.
	for !q.empty() {
		v := q.front()
		q.pop()
		st.push(v)

		for _, w := range g.Adj[v] {
			// If distance from s to w is infinity (-1)
			// then set it and enqueue w.
			if dist[w] < 0 {
				dist[w] = dist[v] + 1
				q.push(w)
			}

			// If w is on a shortest path the update
			// sigma and add v to w's predecessor list.
			if dist[w] == dist[v]+1 {
				sigma[w] += sigma[v]
				pred[w] = append(pred[w], v)
			}
		}
	}

	// delta[v] is the ratio of the shortest paths between s and t that go
	// through v and the total number of shortest paths between s and t.
	// If we have delta then the betweenness centrality is simply the sum
	// of delta[w] for each w != s.
	delta := make([]float64, len(g.Nodes))

	for !st.empty() {
		w := st.top()
		st.pop()

		// pred[w] is the list of nodes that immediately precede w on a
		// shortest path from s.
		for _, v := range pred[w] {
			// Update delta using Brandes' equation.
			delta[v] += (float64(sigma[v]) / float64(sigma[w])) * (1.0 + delta[w])
		}

		if w != s {
			// As noted above centrality is simply the sum
			// of delta[w] for each w != s.
			centrality[w] += delta[w]
		}
	}
}

// Refresh recalculates and stores centrality values.
func (bc *BetweennessCentrality) Refresh(graph ChannelGraph) error {
	cache, err := NewSimpleGraph(graph)
	if err != nil {
		return err
	}

	var wg sync.WaitGroup
	work := make(chan int)
	partials := make(chan []float64, bc.workers)

	// Each worker will compute a partial result.
	// This partial result is a sum of centrality updates
	// on roughly N / workers nodes.
	worker := func() {
		defer wg.Done()
		partial := make([]float64, len(cache.Nodes))

		// Consume the next node, update centrality
		// parital to avoid unnecessary synchronization.
		for node := range work {
			betweennessCentrality(cache, node, partial)
		}
		partials <- partial
	}

	// Now start the N workers.
	wg.Add(bc.workers)
	for i := 0; i < bc.workers; i++ {
		go worker()
	}

	// Distribute work amongst workers.
	// Should be fair when the graph is sufficiently large.
	for node := range cache.Nodes {
		work <- node
	}

	close(work)
	wg.Wait()
	close(partials)

	// Collect and sum partials for final result.
	centrality := make([]float64, len(cache.Nodes))
	for partial := range partials {
		for i := 0; i < len(partial); i++ {
			centrality[i] += partial[i]
		}
	}

	// Get min/max to be able to normalize
	// centrality values between 0 and 1.
	bc.min = 0
	bc.max = 0
	if len(centrality) > 0 {
		for _, v := range centrality {
			if v < bc.min {
				bc.min = v
			} else if v > bc.max {
				bc.max = v
			}
		}
	}

	// Divide by two as this is an undirected graph.
	bc.min /= 2.0
	bc.max /= 2.0

	bc.centrality = make(map[NodeID]float64)
	for u, value := range centrality {
		// Divide by two as this is an undirected graph.
		bc.centrality[cache.Nodes[u]] = value / 2.0
	}

	return nil
}

// GetMetric returns the current centrality values for each node indexed
// by node id.
func (bc *BetweennessCentrality) GetMetric(normalize bool) map[NodeID]float64 {
	// Normalization factor.
	var z float64
	if (bc.max - bc.min) > 0 {
		z = 1.0 / (bc.max - bc.min)
	}

	centrality := make(map[NodeID]float64)
	for k, v := range bc.centrality {
		if normalize {
			v = (v - bc.min) * z
		}
		centrality[k] = v
	}

	return centrality
}
