package spimedb.util.bag;
import com.google.common.base.Joiner;
import jcog.data.sorted.SortedArray;
import jcog.table.SortedListTable;
import org.apache.commons.lang3.mutable.MutableFloat;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* priority queue
*/
public class PriBag<V> extends SortedListTable<V, Budget<V>> implements BiFunction<Budget<V>, Budget<V>, Budget<V>> {
public final BudgetMerge mergeFunction;
/**
* inbound pressure sum since last commit
*/
public volatile float pressure = 0;
/**
* if you dont manually call commit at periodic times (ex: forgetting updates),
* then use autocommit to update the bag after each insertion
*/
private static final boolean autocommit = true;
public PriBag(int cap, BudgetMerge mergeFunction, /*@NotNull*/ Map<V, Budget<V>> map) {
super(new SortedArray<>(), map);
this.mergeFunction = mergeFunction;
this.capacity = cap;
}
@Override
public float floatValueOf(Budget<V> x) {
return -pCmp(x);
}
public boolean containsKey(Object o) {
return map.containsKey(o);
}
@Nullable
@Override
public Budget<V> remove(/*@NotNull*/ V x) {
Budget<V> b = super.remove(x);
if (b != null) {
onRemoved(b);
}
return b;
}
/**
* returns whether the capacity has changed
*/
//@Override
public final boolean setCapacity(int newCapacity) {
if (newCapacity != this.capacity) {
synchronized (_items()) {
this.capacity = newCapacity;
if (this.size() > newCapacity)
commit(null);
}
return true;
}
return false;
}
//@Override
public final boolean isEmpty() {
return size() == 0;
}
/**
* returns true unless failed to add during 'add' operation
*/
//@Override
protected boolean updateItems(@Nullable Budget<V> toAdd) {
SortedArray<Budget<V>> items = this.items;
//List<BLink<V>> pendingRemoval;
List<Budget> pendingRemoval;
boolean result;
synchronized (items) {
int additional = (toAdd != null) ? 1 : 0;
int c = capacity();
int s = size();
int nextSize = s + additional;
if (nextSize > c) {
pendingRemoval = new ArrayList(nextSize - c);
s = clean(toAdd, s, nextSize - c, pendingRemoval);
if (s + additional > c) {
clean2(pendingRemoval);
return false; //throw new RuntimeException("overflow");
}
} else {
pendingRemoval = null;
}
if (toAdd != null) {
//append somewhere in the items; will get sorted to appropriate location during next commit
//TODO update range
// Object[] a = items.array();
// //scan for an empty slot at or after index 's'
// for (int k = s; k < a.length; k++) {
// if ((a[k] == null) /*|| (((BLink)a[k]).isDeleted())*/) {
// a[k] = toAdd;
// items._setSize(s+1);
// return;
// }
// }
int ss = size();
if (ss < c) {
items.add(toAdd, this);
result = true;
//items.addInternal(toAdd); //grows the list if necessary
} else {
//throw new RuntimeException("list became full during insert");
map.remove(toAdd.id);
result = false;
}
// float p = toAdd.pri;
// if (minPri < p && capacity()<=size()) {
// this.minPri = p;
// }
} else {
result = size() > 0;
}
}
if (pendingRemoval != null)
clean2(pendingRemoval);
return result;
// if (toAdd != null) {
// synchronized (items) {
// //the item key,value should already be in the map before reaching here
// items.add(toAdd, this);
// }
// modified = true;
// }
//
// if (modified)
// updateRange(); //regardless, this also handles case when policy changed and allowed more capacity which should cause minPri to go to -1
}
private int clean(@Nullable Budget<V> toAdd, int s, int minRemoved, List<Budget> trash) {
final int s0 = s;
if (cleanDeletedEntries()) {
//first step: remove any nulls and deleted values
s -= removeDeleted(trash, minRemoved);
if (s0 - s >= minRemoved)
return s;
}
//second step: if still not enough, do a hardcore removal of the lowest ranked items until quota is met
s = removeWeakestUntilUnderCapacity(s, trash, toAdd != null);
return s;
}
/**
* return whether to clean deleted entries prior to removing any lowest ranked items
*/
protected static boolean cleanDeletedEntries() {
return false;
}
private void clean2(List<Budget> trash) {
int toRemoveSize = trash.size();
if (toRemoveSize > 0) {
for (int i = 0; i < toRemoveSize; i++) {
Budget w = trash.get(i);
map.remove(w.id);
// if (k2 != w && k2 != null) {
// //throw new RuntimeException(
// logger.error("bag inconsistency: " + w + " removed but " + k2 + " may still be in the items list");
// //reinsert it because it must have been added in the mean-time:
// map.putIfAbsent(k, k2);
// }
//pressure -= w.priIfFiniteElseZero(); //release pressure
onRemoved(w);
w.delete();
}
}
}
/**
* called on eviction
*/
protected void onRemoved(Budget<V> w) {
}
private int removeWeakestUntilUnderCapacity(int s, /*@NotNull*/ List<Budget> toRemove, boolean pendingAddition) {
SortedArray<Budget<V>> items = this.items;
final int c = capacity;
while (!isEmpty() && ((s - c) + (pendingAddition ? 1 : 0)) > 0) {
Budget<V> w = items.remove(s - 1);
if (w != null) //skip over nulls
toRemove.add(w);
s--;
}
return s;
}
@Nullable
//@Override
public V activate(Object key, float toAdd) {
Budget<V> c = map.get(key);
if (c != null && !c.isDeleted()) {
//float dur = c.dur();
float pBefore = c.pri;
c.priAdd(toAdd);
float delta = c.pri - pBefore;
pressure += delta;// * dur;
return c.id;
}
return null;
}
public void mul(float factor) {
forEach(b->b.priMult(factor));
}
public void add(float inc) {
forEach(b->b.priAdd(inc));
}
//@Override
public V mul(Object key, float factor) {
Budget<V> c = map.get(key);
if (c != null) {
float pBefore = c.pri;
if (pBefore != pBefore)
return null; //already deleted
c.priMult(factor);
float delta = c.pri - pBefore;
pressure += delta;// * dur;
return c.id;
}
return null;
}
// //@Override
// public final int compare(@Nullable BLink o1, @Nullable BLink o2) {
// float f1 = cmp(o1);
// float f2 = cmp(o2);
//
// if (f1 < f2)
// return 1; // Neither val is NaN, thisVal is smaller
// if (f1 > f2)
// return -1; // Neither val is NaN, thisVal is larger
// return 0;
// }
/**
* true iff o1 > o2
*/
static final boolean cmpGT(@Nullable Budget o1, @Nullable Budget o2) {
return cmpGT(o1, pCmp(o2));
}
static final boolean cmpGT(@Nullable Budget o1, float o2) {
return (pCmp(o1) < o2);
}
/**
* true iff o1 > o2
*/
static final boolean cmpGT(float o1, @Nullable Budget o2) {
return (o1 < pCmp(o2));
}
/**
* true iff o1 < o2
*/
static final boolean cmpLT(@Nullable Budget o1, @Nullable Budget o2) {
return cmpLT(o1, pCmp(o2));
}
static final boolean cmpLT(@Nullable Budget o1, float o2) {
return (pCmp(o1) > o2);
}
/**
* gets the scalar float value used in a comparison of BLink's
* essentially the same as b.priIfFiniteElseNeg1 except it also includes a null test. otherwise they are interchangeable
*/
static float pCmp(@Nullable Budget b) {
return (b == null) ? -2f : b.pri; //sort nulls beneath
// float p = b.pri;
// return p == p ? p : -1f;
//return (b!=null) ? b.priIfFiniteElseNeg1() : -1f;
//return b.priIfFiniteElseNeg1();
}
//@Override
public final V key(/*@NotNull*/ Budget<V> l) {
return l.id;
}
public final Budget<V> put(/*@NotNull*/ V key, float pri) {
return put(key, pri, null);
}
//@Override
public final Budget<V> put(/*@NotNull*/ V key, float pri, @Nullable MutableFloat overflow) {
if (pri < 0) { //already deleted
onRemoved(new Budget<>(key, pri)); //HACK maybe use a separate handler, for onRejected
return null;
}
pressure += pri;
Budget<V> existing = map.get(key);
if (existing != null) {
//result=0
//Budget vv = existing.clone();
if (existing.isDeleted()) {
//it has been deleted.. TODO reinsert?
map.remove(key);
pressure -= pri;
onRemoved(existing);
return null;
}
//re-rank
float o = mergeFunction.merge(existing, pri);
if (autocommit)
sort();
if (o > 0) {
if (overflow != null)
overflow.add(o);
pressure -= o;
}
return existing;
} else {
if (size() >= capacity && pri < priMin() /* < here rather than <= allows flat FIFO replacement */) {
//reject due to insufficient budget
if (overflow != null) {
overflow.add(pri);
}
pressure -= pri;
onRemoved(new Budget<>(key, pri));
return null;
} else {
//accepted for fresh insert
Budget next = new Budget<>(key, pri);
map.put(key, next);
synchronized (items) {
if (updateItems(next)) {
onAdded(next); //success
return next;
} else {
onRemoved(next);
return null;
}
}
}
}
}
protected void onAdded(Budget<V> w) {
}
// /**
// * the applied budget will not become effective until commit()
// */
// /*@NotNull*/
// protected final void putExists(/*@NotNull*/ Budgeted b, float scale, /*@NotNull*/ BLink<V> existing, @Nullable MutableFloat overflow) {
//
//
//
// }
// /*@NotNull*/
// protected final BLink<V> newLink(/*@NotNull*/ V i, /*@NotNull*/ Budgeted b) {
// return newLink(i, b, 1f);
// }
// /*@NotNull*/
// protected final BLink<V> newLink(/*@NotNull*/ V i, /*@NotNull*/ Budgeted b, float scale) {
// return newLink(i, scale * b.pri, b.dur(), b.qua());
// }
@Nullable
//@Override
protected final Budget<V> addItem(Budget<V> i) {
throw new UnsupportedOperationException();
}
/*@NotNull*/
public final PriBag<V> commit(@Nullable Function<PriBag, Consumer<Budget>> update) {
synchronized (items) {
update(update != null ? update.apply(this) : null);
}
return this;
}
public float mass() {
float mass = 0;
synchronized (items) {
int iii = size();
for (int i = 0; i < iii; i++) {
Budget x = get(i);
if (x != null)
mass += x.priSafe(0);
}
}
return mass;
}
/**
* applies the 'each' consumer and commit simultaneously, noting the range of items that will need sorted
*/
/*@NotNull*/
protected PriBag<V> update(@Nullable Consumer<Budget> each) {
synchronized (items) {
if (each != null)
this.pressure = 0; //reset pressure accumulator
if (size() > 0) {
if (updateItems(null)) {
updateBudget(each);
}
sort();
}
}
return this;
}
public void sort() {
int size = size();
if (size > 1)
qsort(new short[16 /* estimate */], items.array(), (short) 0 /*dirtyStart - 1*/, (short) (size - 1));
}
/**
* returns the index of the lowest unsorted item
*/
private void updateBudget(@Nullable Consumer<Budget> each) {
// int dirtyStart = -1;
int s = size();
Budget<V>[] l = items.array();
int i = s - 1;
for (; i >= 0; ) {
Budget<V> b = l[i];
float bCmp;
bCmp = b != null ? b.priSafe(-1) : -2; //sort nulls to the end of the end
if (bCmp > 0) {
if (each != null)
each.accept(b);
}
i--;
}
}
private int removeDeleted(/*@NotNull*/ List<Budget> removed, int minRemoved) {
SortedArray<Budget<V>> items = this.items;
final Object[] l = items.array();
int removedFromMap = 0;
//iterate in reverse since null entries should be more likely to gather at the end
for (int s = size() - 1; removedFromMap < minRemoved && s >= 0; s--) {
Budget x = (Budget) l[s];
if (x == null || x.isDeleted()) {
items.removeFast(s);
if (x != null)
removed.add(x);
removedFromMap++;
}
}
return removedFromMap;
}
//@Override
public void clear() {
synchronized (items) {
//map is possibly shared with another bag. only remove the items from it which are present in items
items.forEach(x -> {
map.remove(x.id);
onRemoved(x);
});
items.clear();
}
}
@Nullable
@Override
public Budget apply(@Nullable Budget bExisting, Budget bNext) {
if (bExisting != null) {
mergeFunction.merge(bExisting, bNext.pri);
return bExisting;
} else {
return bNext;
}
}
//@Override
public void forEach(Consumer<? super Budget<V>> action) {
Object[] x = items.array();
if (x.length > 0) {
for (Budget a : ((Budget[]) x)) {
if (a != null) {
Budget<V> b = a;
if (!b.isDeleted())
action.accept(b);
}
}
}
}
/**
* http://kosbie.net/cmu/summer-08/15-100/handouts/IterativeQuickSort.java
*/
static void qsort(short[] stack, Budget[] c, short left, short right) {
int stack_pointer = -1;
int cLenMin1 = c.length - 1;
while (true) {
short i, j;
if (right - left <= 7) {
Budget swap;
//bubble sort on a region of right less than 8?
for (j = (short) (left + 1); j <= right; j++) {
swap = c[j];
i = (short) (j - 1);
float swapV = pCmp(swap);
while (i >= left && cmpGT(c[i], swapV)) {
swap(c, (short) (i + 1), i);
i--;
}
c[i + 1] = swap;
}
if (stack_pointer != -1) {
right = stack[stack_pointer--];
left = stack[stack_pointer--];
} else {
break;
}
} else {
Budget swap;
short median = (short) ((left + right) / 2);
i = (short) (left + 1);
j = right;
swap(c, i, median);
if (cmpGT(c[left], c[right])) {
swap(c, right, left);
}
if (cmpGT(c[i], c[right])) {
swap(c, right, i);
}
if (cmpGT(c[left], c[i])) {
swap(c, i, left);
}
{
Budget temp = c[i];
float tempV = pCmp(temp);
while (true) {
while (i < cLenMin1 && cmpLT(c[++i], tempV)) ;
while (cmpGT(c[--j], tempV)) ;
if (j < i) {
break;
}
swap(c, j, i);
}
c[left + 1] = c[j];
c[j] = temp;
}
short a, b;
if ((right - i + 1) >= (j - left)) {
a = i;
b = right;
right = (short) (j - 1);
} else {
a = left;
b = (short) (j - 1);
left = i;
}
stack[++stack_pointer] = a;
stack[++stack_pointer] = b;
}
}
}
static void swap(Budget[] c, short x, short y) {
Budget swap;
swap = c[y];
c[y] = c[x];
c[x] = swap;
}
/*@NotNull*/
@Override
public String toString() {
return Joiner.on(", ").join(items);// + '{' + items.getClass().getSimpleName() + '}';
}
static float itemOrZeroIfNull(Budget x) {
return x != null ? x.pri : 0f;
}
//@Override
public float priMax() {
return itemOrZeroIfNull(items.first());
}
//@Override
public float priMin() {
return itemOrZeroIfNull(items.last());
}
public float pri(V id, float valueIfAbsent) {
Budget<V> b = map.get(id);
if (b == null) {
return valueIfAbsent;
}
return b.pri;
}
}