//  Copyright Istio Authors
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

package loadbalancer

import (
	"container/heap"
)

// Entry is an item for load balance
type Entry struct {
	deadline float64
	index    int64
	value    any
	weight   float64
}

// priorityQueue is a queue that always pop the highest priority item
type priorityQueue []*Entry

// Len implements heap.Interface/sort.Interface
func (pq priorityQueue) Len() int { return len(pq) }

// Less implements heap.Interface/sort.Interface
func (pq priorityQueue) Less(i, j int) bool {
	// Flip logic to make this a min queue.
	if pq[i].deadline == pq[j].deadline {
		return pq[i].index < pq[j].index
	}
	return pq[i].deadline < pq[j].deadline
}

// Swap implements heap.Interface/sort.Interface
func (pq priorityQueue) Swap(i, j int) {
	pq[i], pq[j] = pq[j], pq[i]
}

// Push implements heap.Interface for pushing an item into the heap
func (pq *priorityQueue) Push(x any) {
	entry := x.(*Entry)
	*pq = append(*pq, entry)
}

// Pop implements heap.Interface for poping an item from the heap
func (pq *priorityQueue) Pop() any {
	old := *pq
	n := len(old)
	entry := old[n-1]
	*pq = old[0 : n-1]
	return entry
}

// EDF implements the Earliest Deadline First scheduling algorithm
type EDF struct {
	pq              *priorityQueue
	currentIndex    int64
	currentDeadline float64
}

// Add a new entry for load balance
func (e *EDF) Add(weight float64, value any) {
	e.currentIndex++
	heap.Push(e.pq, &Entry{
		value:    value,
		weight:   weight,
		deadline: e.currentDeadline + 1/weight,
		index:    e.currentIndex,
	})
}

// PickAndAdd picks an available entry and re-adds it with the given weight calculation
func (e *EDF) PickAndAdd(calcWeight func(prevWeight float64, value any) float64) any {
	// if no available entry, return nil
	if len(*e.pq) == 0 {
		return nil
	}
	entry := heap.Pop(e.pq).(*Entry)
	// currentDeadline should be entry's deadline so that new added entry would have a fair
	// competition environment with the old ones
	e.currentDeadline = entry.deadline

	// Re-add it with the updated weight.
	e.Add(calcWeight(entry.weight, entry.value), entry.value)
	return entry.value
}

// NewEDF create a new edf scheduler
func NewEDF() *EDF {
	pq := make(priorityQueue, 0)
	return &EDF{
		pq:           &pq,
		currentIndex: 0,
	}
}
