package freenet.support;

import java.util.Arrays;

import freenet.client.async.ClientContext;
import freenet.client.async.ClientRequestSelector;
import freenet.client.async.RequestSelectionTreeNode;

/**
 * Like RandomGrabArray, but there is an equal chance of any given client's requests being
 * returned. Again, not persistent; this is reconstructed on restart.
 * 
 * LOCKING: There is a single lock for the entire tree, the ClientRequestSelector. This must be 
 * taken before calling any methods on RGA or SRGA. See the javadocs there for deeper explanation.
 * 
 * A lot of this is over-complicated and over-expensive because of db4o. A lot of it is O(n).
 * This is all kept in RAM now so we can change it at will, plus there is only one object 
 * queued per splitfile now, so memory pressure is much less of an issue. 
 * FIXME Simplify and improve performance!
 */
public class SectoredRandomGrabArray<T, C extends RemoveRandomWithObject<T>> implements RemoveRandom, RemoveRandomParent, RequestSelectionTreeNode {
	private static volatile boolean logMINOR;
	
	static {
		Logger.registerClass(SectoredRandomGrabArray.class);
	}

	protected RemoveRandomWithObject<T>[] grabArrays;
	private T[] grabClients;
	private RemoveRandomParent parent;
	protected final ClientRequestSelector root;
	private long wakeupTime;
	
	public SectoredRandomGrabArray(RemoveRandomParent parent, ClientRequestSelector root) {
		grabClients = (T[]) new Object[0];
		grabArrays = new RemoveRandomWithObject[0];
		this.parent = parent;
		this.root = root;
	}

	protected void addElement(T client, C rga) {
	    synchronized(root) {
		final int len = grabArrays.length;

		grabArrays = Arrays.copyOf(grabArrays, len+1);
		grabArrays[len] = rga;
		
		grabClients = Arrays.copyOf(grabClients, len+1);
		grabClients[len] = client;
	    }
	}

	protected int haveClient(T client) {
	    synchronized(root) {
		for(int i=0;i<grabClients.length;i++) {
			if(grabClients[i] == client) return i;
		}
		return -1;
	    }
	}

	/**
	 * Get a grabber.
	 */
	public C getGrabber(T client) {
	    synchronized(root) {
		int idx = haveClient(client);
		if(idx == -1) return null;
		else return (C) grabArrays[idx];
	    }
	}
	
	public T getClient(int x) {
	    synchronized(root) {
		return grabClients[x];
	    }
	}

	/**
	 * Put a grabber.
	 */
	public void addGrabber(T client, C requestGrabber, ClientContext context) {
	    synchronized(root) {
		if(requestGrabber.getObject() != client)
			throw new IllegalArgumentException("Client not equal to RemoveRandomWithObject's client: client="+client+" rr="+requestGrabber+" his object="+requestGrabber.getObject());
		addElement(client, requestGrabber);
		if(context != null) {
		    clearWakeupTime(context);
		}
	    }
	}

	@Override
	public RemoveRandomReturn removeRandom(RandomGrabArrayItemExclusionList excluding, ClientContext context, long now) {
	    synchronized(root) {
		while(true) {
			if(grabArrays.length == 0) return null;
			if(grabArrays.length == 1) {
				return removeRandomOneOnly(excluding, context, now);
			}
			if(grabArrays.length == 2) {
				RemoveRandomReturn ret = removeRandomTwoOnly(excluding, context, now);
				if(ret == null) continue; // Go around loop again, it has reduced to 1 or 0.
				return ret;
			}
			RandomGrabArrayItem item = removeRandomLimited(excluding, context, now);
			if(item != null)
				return new RemoveRandomReturn(item);
			else
				return removeRandomExhaustive(excluding, context, now);
		}
	    }
	}

	private RemoveRandomReturn removeRandomExhaustive(
			RandomGrabArrayItemExclusionList excluding,
			ClientContext context, long now) {
	    synchronized(root) {
		long wakeupTime = Long.MAX_VALUE;
		if(grabArrays.length == 0) return null;
		int x = context.fastWeakRandom.nextInt(grabArrays.length);
		for(int i=0;i<grabArrays.length;i++) {
			x++;
			if(x >= grabArrays.length) x = 0;
			RemoveRandomWithObject<T> rga = grabArrays[x];
			long excludeTime = rga.getWakeupTime(context, now);
			if(excludeTime > 0) {
				if(wakeupTime > excludeTime) wakeupTime = excludeTime;
				continue;
			}
			if(logMINOR)
				Logger.minor(this, "Picked "+x+" of "+grabArrays.length+" : "+rga+" on "+this);
			
			RandomGrabArrayItem item = null;
			RemoveRandomReturn val = rga.removeRandom(excluding, context, now);
			if(val != null) {
				if(val.item != null)
					item = val.item;
				else {
					if(wakeupTime > val.wakeupTime) wakeupTime = val.wakeupTime;
				}
			}
			if(logMINOR)
				Logger.minor(this, "RGA has picked "+x+"/"+grabArrays.length+": "+item+
						" rga.isEmpty="+rga.isEmpty());
			if(item != null) {
				return new RemoveRandomReturn(item);
			} else if(rga.isEmpty()) {
				if(logMINOR)
					Logger.minor(this, "Removing grab array "+x+" : "+rga+" (is empty)");
				removeElement(x);
			}
		}
		reduceWakeupTime(wakeupTime, context);
		return new RemoveRandomReturn(wakeupTime);
	    }
	}

	private RandomGrabArrayItem removeRandomLimited(
			RandomGrabArrayItemExclusionList excluding,
			ClientContext context, long now) {
	    synchronized(root) {
		/** Count of arrays that have items but didn't return anything because of exclusions */
		final int MAX_EXCLUDED = 10;
		int excluded = 0;
		while(true) {
			if(grabArrays.length == 0) return null;
			int x = context.fastWeakRandom.nextInt(grabArrays.length);
			RemoveRandomWithObject<T> rga = grabArrays[x];
			if(rga == null) {
				// We handle this in the other cases so we should handle it here.
				Logger.error(this, "Slot "+x+" is null for client "+grabClients[x]);
				excluded++;
				if(excluded > MAX_EXCLUDED) {
					Logger.normal(this, "Too many sub-arrays are entirely excluded on "+this+" length = "+grabArrays.length, new Exception("error"));
					return null;
				}
				continue;
			}
			long excludeTime = rga.getWakeupTime(context, now);
			if(excludeTime > 0) {
				excluded++;
				if(excluded > MAX_EXCLUDED) {
					Logger.normal(this, "Too many sub-arrays are entirely excluded on "+this+" length = "+grabArrays.length, new Exception("error"));
					return null;
				}
				continue;
			}
			if(logMINOR)
				Logger.minor(this, "Picked "+x+" of "+grabArrays.length+" : "+rga+" on "+this);
			
			RandomGrabArrayItem item = null;
			RemoveRandomReturn val = rga.removeRandom(excluding, context, now);
			if(val != null && val.item != null) item = val.item;
			if(logMINOR)
				Logger.minor(this, "RGA has picked "+x+"/"+grabArrays.length+": "+item+
						" rga.isEmpty="+rga.isEmpty());
			// If it is not empty but returns null we exclude it, and count the exclusion.
			// If it is empty we remove it, and don't count the exclusion.
			if(item != null) {
				return item;
			} else {
				if(rga.isEmpty()) {
					if(logMINOR)
						Logger.minor(this, "Removing grab array "+x+" : "+rga+" (is empty)");
					removeElement(x);
				} else {
					excluded++;
					if(excluded > MAX_EXCLUDED) {
						Logger.normal(this, "Too many sub-arrays are entirely excluded on "+this+" length = "+grabArrays.length, new Exception("error"));
						return null;
					}
				}
				continue;
			}
		}
	    }
	}

	private RemoveRandomReturn removeRandomTwoOnly(
			RandomGrabArrayItemExclusionList excluding,
			ClientContext context, long now) {
	    synchronized(root) {
		long wakeupTime = Long.MAX_VALUE;
		// Another simple common case
		int x = context.fastWeakRandom.nextBoolean() ? 1 : 0;
		RemoveRandomWithObject<T> rga = grabArrays[x];
		RemoveRandomWithObject<T> firstRGA = rga;
		if(rga == null) {
			Logger.error(this, "rga = null on "+this);
			if(grabArrays[1-x] == null) {
				Logger.error(this, "other rga is also null on "+this);
				grabArrays = (C[]) new RemoveRandomWithObject[0];
				grabClients = (T[]) new Object[0];
				return null;
			} else {
				Logger.error(this, "grabArrays["+(1-x)+"] is valid but ["+x+"] is null, correcting...");
				grabArrays = (C[]) new RemoveRandomWithObject[] { grabArrays[1-x] };
				grabClients = (T[]) new Object[] { grabClients[1-x] };
				return null;
			}
		}
		RandomGrabArrayItem item = null;
		RemoveRandomReturn val = null;
		if(logMINOR) Logger.minor(this, "Only 2, trying "+rga);
		long excludeTime = rga.getWakeupTime(context, now);
		if(excludeTime > 0) {
			wakeupTime = excludeTime;
			rga = null;
			firstRGA = null;
		} else {
			val = rga.removeRandom(excluding, context, now);
			if(val != null) {
				if(val.item != null)
					item = val.item;
				else {
					if(wakeupTime > val.wakeupTime) wakeupTime = val.wakeupTime;
				}
			}
		}
		if(item != null) {
			if(logMINOR)
				Logger.minor(this, "Returning (two items only) "+item+" for "+rga);
			return new RemoveRandomReturn(item);
		} else {
			x = 1-x;
			rga = grabArrays[x];
			if(rga == null) {
				Logger.error(this, "Other RGA is null later on on "+this);
				grabArrays = (C[]) new RemoveRandomWithObject[] { grabArrays[1-x] };
				grabClients = (T[]) new Object[] { grabClients[1-x] };
                reduceWakeupTime(wakeupTime, context);
				return new RemoveRandomReturn(wakeupTime);
			}
			excludeTime = rga.getWakeupTime(context, now);
			if(excludeTime > 0) {
				if(wakeupTime > excludeTime) wakeupTime = excludeTime;
				rga = null;
			} else {
				val = rga.removeRandom(excluding, context, now);
				if(val != null) {
					if(val.item != null)
						item = val.item;
					else {
						if(wakeupTime > val.wakeupTime) wakeupTime = val.wakeupTime;
					}
				}
			}
			if(firstRGA != null && firstRGA.isEmpty() && rga != null && rga.isEmpty()) {
				if(logMINOR) Logger.minor(this, "Removing both on "+this+" : "+firstRGA+" and "+rga+" are empty");
				grabArrays = (C[]) new RemoveRandomWithObject[0];
				grabClients = (T[]) new Object[0];
			} else if(firstRGA != null && firstRGA.isEmpty()) {
				if(logMINOR) Logger.minor(this, "Removing first: "+firstRGA+" is empty on "+this);
				grabArrays = (C[]) new RemoveRandomWithObject[] { grabArrays[x] }; // don't use RGA, it may be nulled out
				grabClients = (T[]) new Object[] { grabClients[x] };
			}
			if(logMINOR)
				Logger.minor(this, "Returning (two items only) "+item+" for "+rga);
			if(item == null) {
				if(grabArrays.length == 0)
					return null; // Remove this as well
                reduceWakeupTime(wakeupTime, context);
				return new RemoveRandomReturn(wakeupTime);
			} else return new RemoveRandomReturn(item);
		}
	    }
	}

	private RemoveRandomReturn removeRandomOneOnly(
			RandomGrabArrayItemExclusionList excluding,
			ClientContext context, long now) {
	    synchronized(root) {
		long wakeupTime = Long.MAX_VALUE;
		// Optimise the common case
		RemoveRandomWithObject<T> rga = grabArrays[0];
		if(logMINOR) Logger.minor(this, "Only one RGA: "+rga);
		long excludeTime = rga.getWakeupTime(context, now);
		if(excludeTime > 0)
			return new RemoveRandomReturn(excludeTime);
		if(rga == null) {
			Logger.error(this, "Only one entry and that is null");
			// We are sure
			grabArrays = (C[]) new RemoveRandomWithObject[0];
			grabClients = (T[]) new Object[0];
			return null;
		}
		RemoveRandomReturn val = rga.removeRandom(excluding, context, now);
		RandomGrabArrayItem item = null;
		if(val != null) { // val == null => remove it
			if(val.item != null)
				item = val.item;
			else {
				wakeupTime = val.wakeupTime;
			}
		}
		if(rga.isEmpty()) {
			if(logMINOR)
				Logger.minor(this, "Removing only grab array (0) : "+rga);
			grabArrays = (C[]) new RemoveRandomWithObject[0];
			grabClients = (T[]) new Object[0];
		}
		if(logMINOR)
			Logger.minor(this, "Returning (one item only) "+item+" for "+rga);
		if(item == null) {
			if(grabArrays.length == 0) {
				if(logMINOR) Logger.minor(this, "Arrays are empty on "+this);
				return null; // Remove this as well
			}
            reduceWakeupTime(wakeupTime, context);
			return new RemoveRandomReturn(wakeupTime);
		} else return new RemoveRandomReturn(item);
	    }
	}

	private void removeElement(int x) {
	    synchronized(root) {
		final int grabArraysLength = grabArrays.length;
		int newLen = grabArraysLength > 1 ? grabArraysLength-1 : 0;
		C[] newArray = (C[]) new RemoveRandomWithObject[newLen];
		if(x > 0)
			System.arraycopy(grabArrays, 0, newArray, 0, x);
		if(x < grabArraysLength-1)
			System.arraycopy(grabArrays, x+1, newArray, x, grabArraysLength - (x+1));
		grabArrays = newArray;
		
		Object[] newClients = new Object[newLen];
		if(x > 0)
			System.arraycopy(grabClients, 0, newClients, 0, x);
		if(x < grabArraysLength-1)
			System.arraycopy(grabClients, x+1, newClients, x, grabArraysLength - (x+1));
		grabClients = (T[]) newClients;
	    }
	}

	public boolean isEmpty() {
	    synchronized(root) {
		return grabArrays.length == 0;
	    }
	}
	
	public int size() {
	    synchronized(root) {
		return grabArrays.length;
	    }
	}
	
	@Override
	public void maybeRemove(RemoveRandom r, ClientContext context) {
		int count = 0;
		int finalSize;
		synchronized(root) {
			while(true) {
				int found = -1;
				for(int i=0;i<grabArrays.length;i++) {
					if(grabArrays[i] == r) {
						found = i;
						break;
					}
				}
				if(found != -1) {
					count++;
					if(count > 1) Logger.error(this, "Found "+r+" many times in "+this, new Exception("error"));
					removeElement(found);
				} else {
					break;
				}
			}
			finalSize = grabArrays.length;
		}
		if(count == 0) {
			// This is not unusual, it was e.g. removed because of being empty.
			// And it has already been removeFrom()'ed.
			if(logMINOR) Logger.minor(this, "Not in parent: "+r+" for "+this, new Exception("error"));
		}
		if(finalSize == 0 && parent != null) {
			parent.maybeRemove(this, context);
		}
	}

	@Override
	public void setParent(RemoveRandomParent newParent) {
	    synchronized(root) {
		this.parent = newParent;
	    }
	}

    @Override
    public RequestSelectionTreeNode getParentGrabArray() {
        synchronized(root) {
            return parent;
        }
    }
	
    @Override
    public long getWakeupTime(ClientContext context, long now) {
        synchronized(root) {
            if(wakeupTime < now) wakeupTime = 0;
            return wakeupTime;
        }
    }
    
    @Override
    public boolean reduceWakeupTime(long wakeupTime, ClientContext context) {
        if(logMINOR) Logger.minor(this, "reduceCooldownTime("+(wakeupTime-System.currentTimeMillis())+") on "+this);
        boolean reachedRoot = false;
        synchronized(root) {
            if(this.wakeupTime > wakeupTime) {
                this.wakeupTime = wakeupTime;
                if(parent != null) parent.reduceWakeupTime(wakeupTime, context);
                else reachedRoot = true; // Even if it reduces it we need to wake it up.
            } else return false;
        }
        if(reachedRoot)
            root.wakeUp(context);
        return true;
    }
    
    @Override
    public void clearWakeupTime(ClientContext context) {
        if(logMINOR) Logger.minor(this, "clearCooldownTime() on "+this);
        synchronized(root) {
            wakeupTime = 0;
            if(parent != null) parent.clearWakeupTime(context);
        }
    }
    
}
