package freenet.support;

import static java.util.concurrent.TimeUnit.MINUTES;

import java.util.Arrays;

import org.tanukisoftware.wrapper.WrapperManager;

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

/**
 * An array which supports very fast remove-and-return-a-random-element.
 * 
 * This is *NOT* persistent. The request selection structures are reconstructed on restart. However
 * it used to be, and probably has a lot of cruft and inefficiency as a result. 
 * 
 * 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.
 * 
 * FIXME Simplify and improve performance. A lot of this is O(n), and this should probably be fixed. 
 * Memory usage was an issue but probably isn't now given that the individual items are now quite 
 * large (entire splitfiles or at least entire segments).
 */
public class RandomGrabArray implements RemoveRandom, RequestSelectionTreeNode {
	private static volatile boolean logMINOR;
	
	static {
		Logger.registerClass(RandomGrabArray.class);
	}
	
	private static class Block {
		RandomGrabArrayItem[] reqs;
	}
	
	/** Array of items. Non-null's followed by null's. 
	 * We used to have a Set so we could check whether something is in the set quickly.
	 * We got rid of this because for persistent requests it is vastly faster to just loop the
	 * loop and check ==, and for non-persistent requests it doesn't matter much. */
	private Block[] blocks;
	/** Index of first null item. */
	private int index;
	private final static int MIN_SIZE = 32;
	private final static int BLOCK_SIZE = 1024;
	private final int hashCode;
	private RemoveRandomParent parent;
	protected ClientRequestSelector root;
	private long wakeupTime;

	public RandomGrabArray(RemoveRandomParent parent, ClientRequestSelector root) {
		this.blocks = new Block[] { new Block() };
		blocks[0].reqs = new RandomGrabArrayItem[MIN_SIZE];
		index = 0;
		this.hashCode = super.hashCode();
		this.parent = parent;
		this.root = root;
	}
	
	@Override
	public int hashCode() {
		return hashCode;
	}
	
	public void add(RandomGrabArrayItem req, ClientContext context) {
		if(context != null && req.getWakeupTime(context, System.currentTimeMillis()) < 0) { 
			if(logMINOR) Logger.minor(this, "Is finished already: "+req);
			return;
		}
		req.setParentGrabArray(this); // will store() self
		synchronized(root) {
			if(context != null) {
			    clearWakeupTime(context);
			}
			int x = 0;
			if(blocks.length == 1 && index < BLOCK_SIZE) {
				for(int i=0;i<index;i++) {
					if(blocks[0].reqs[i] == req) {
						return;
					}
				}
				if(index >= blocks[0].reqs.length) {
					blocks[0].reqs = Arrays.copyOf(blocks[0].reqs, Math.min(BLOCK_SIZE, blocks[0].reqs.length*2));
				}
				blocks[0].reqs[index++] = req;
				if(logMINOR) Logger.minor(this, "Added "+req+" before index "+index);
				return;
			}
			int targetBlock = index / BLOCK_SIZE;
			for(int i=0;i<blocks.length;i++) {
				Block block = blocks[i];
				if(i != (blocks.length - 1) && block.reqs.length != BLOCK_SIZE) {
					Logger.error(this, "Block "+i+" of "+blocks.length+" is wrong size: "+block.reqs.length+" should be "+BLOCK_SIZE);
				}
				for(int j=0;j<block.reqs.length;j++) {
					if(x >= index) break;
					if(block.reqs[j] == req) {
						if(logMINOR) Logger.minor(this, "Already contains "+req+" : "+this+" size now "+index);
						return;
					}
					if(block.reqs[j] == null) {
						Logger.error(this, "reqs["+i+"."+j+"] = null on "+this);
					}
					x++;
				}
			}
			if(blocks.length <= targetBlock) {
				if(logMINOR)
					Logger.minor(this, "Adding blocks on "+this);
				Block[] newBlocks = Arrays.copyOf(blocks, targetBlock+1);
				for(int i=blocks.length;i<newBlocks.length;i++) {
					newBlocks[i] = new Block();
					newBlocks[i].reqs = new RandomGrabArrayItem[BLOCK_SIZE];
				}
				blocks = newBlocks;
			}
			Block target = blocks[targetBlock];
			target.reqs[index++ % BLOCK_SIZE] = req;
			if(logMINOR) Logger.minor(this, "Added: "+req+" to "+this+" size now "+index);
		}
	}
	
	/** Must be less than BLOCK_SIZE */
	static final int MAX_EXCLUDED = 10;
	
	@Override
	public RemoveRandomReturn removeRandom(RandomGrabArrayItemExclusionList excluding, ClientContext context, long now) {
		if(logMINOR) Logger.minor(this, "removeRandom() on "+this+" index="+index);
		synchronized(root) {
			if(index == 0) {
				if(logMINOR) Logger.minor(this, "All null on "+this);
				return null;
			}
			if(index < MAX_EXCLUDED) {
				return removeRandomExhaustiveSearch(excluding, context, now);
			}
			RandomGrabArrayItem ret = removeRandomLimited(excluding, context, now);
			if(ret != null)
				return new RemoveRandomReturn(ret);
			if(index == 0) {
				if(logMINOR) Logger.minor(this, "All null on "+this);
				return null;
			}
			return removeRandomExhaustiveSearch(excluding, context, now);
		}
	}
	
	private RandomGrabArrayItem removeRandomLimited(
			RandomGrabArrayItemExclusionList excluding,
			ClientContext context, long now) {
		int excluded = 0;
		while(true) {
			int i = context.fastWeakRandom.nextInt(index);
			int blockNo = i / BLOCK_SIZE;
			RandomGrabArrayItem ret, oret;
			ret = blocks[blockNo].reqs[i % BLOCK_SIZE];
			if(ret == null) {
				Logger.error(this, "reqs["+i+"] = null");
				remove(blockNo, i);
				continue;
			}
			if(ret.getWakeupTime(context, now) > 0) {
				excluded++;
				if(excluded > MAX_EXCLUDED) {
					return null;
				}
				continue;
			}
			oret = ret;
			long itemWakeTime = ret.getWakeupTime(context, now);
			if(itemWakeTime == -1) {
				if(logMINOR) Logger.minor(this, "Not returning because cancelled: "+ret);
				ret = null;
				// Will be removed in the do{} loop
				// Tell it that it's been removed first.
				oret.setParentGrabArray(null);
			}
			if(itemWakeTime == 0)
				itemWakeTime = excluding.exclude(ret, context, now);
			if(ret != null && itemWakeTime > 0) {
				excluded++;
				if(excluded > MAX_EXCLUDED) {
					return null;
				}
				continue;
			}
			if(ret != null) {
				if(logMINOR) Logger.minor(this, "Returning (cannot remove): "+ret+" of "+index);
				return ret;
			}
			// Remove an element.
			do {
				remove(blockNo, i);
				oret = blocks[blockNo].reqs[i % BLOCK_SIZE];
				// Check for nulls, but don't check for cancelled, since we'd have to activate.
			} while (index > i && oret == null);
			int newBlockCount;
			// Shrink array
			if(blocks.length == 1 && index < blocks[0].reqs.length / 4 && blocks[0].reqs.length > MIN_SIZE) {
				// Shrink array
				blocks[0].reqs = Arrays.copyOf(blocks[0].reqs, Math.max(index * 2, MIN_SIZE));
			} else if(blocks.length > 1 &&
					(newBlockCount = (((index + (BLOCK_SIZE/2)) / BLOCK_SIZE) + 1)) < 
					blocks.length) {
				if(logMINOR)
					Logger.minor(this, "Shrinking blocks on "+this);
				blocks = Arrays.copyOf(blocks, newBlockCount);
			}
			return ret;
		}
	}

	private RemoveRandomReturn removeRandomExhaustiveSearch(
			RandomGrabArrayItemExclusionList excluding,
			ClientContext context, long now) {
		if(logMINOR)
			Logger.minor(this, "Doing exhaustive search and compaction on "+this);
		long wakeupTime = Long.MAX_VALUE;
		RandomGrabArrayItem ret = null;
		int random = -1;
		while(true) {
			RandomGrabArrayItem[] reqsReading = blocks[0].reqs;
			RandomGrabArrayItem[] reqsWriting = blocks[0].reqs;
			int blockNumReading = 0;
			int blockNumWriting = 0;
			int offset = -1;
			int writeOffset = -1;
			int exclude = 0;
			int valid = 0;
			int validIndex = -1;
			int target = 0;
			RandomGrabArrayItem chosenItem = null;
			RandomGrabArrayItem validItem = null;
			for(int i=0;i<index;i++) {
				offset++;
				// Compact the array.
				RandomGrabArrayItem item;
				if(offset == BLOCK_SIZE) {
					offset = 0;
					blockNumReading++;
					reqsReading = blocks[blockNumReading].reqs;
				}
				item = reqsReading[offset];
				if(item == null) {
					if(logMINOR) Logger.minor(this, "Found null item at offset "+offset+" i="+i+" block = "+blockNumReading+" on "+this);
					continue;
				}
				boolean excludeItem = false;
				long itemWakeTime = item.getWakeupTime(context, now);
				if (itemWakeTime > 0) {
					// The item is in cooldown, will be wanted later.
					excludeItem = true;
					if (itemWakeTime < wakeupTime) {
						wakeupTime = itemWakeTime;
					}
				} else if (itemWakeTime == -1) {
					// The item is no longer needed and should be removed.
					if(logMINOR) {
						Logger.minor(this, "Removing "+item+" on "+this);
					}
					// We are doing compaction here. We don't need to swap with the end; we write valid ones to the target location.
					reqsReading[offset] = null;
					item.setParentGrabArray(null);
					continue;
				} else {
					long excludeTime = excluding.exclude(item, context, now);
					if (excludeTime > 0) {
						excludeItem = true;
						if(excludeTime < wakeupTime) {
							wakeupTime = excludeTime;
						}
					}
				}
				writeOffset++;
				if(writeOffset == BLOCK_SIZE) {
					writeOffset = 0;
					blockNumWriting++;
					reqsWriting = blocks[blockNumWriting].reqs;
				}
				if(i != target) {
					reqsReading[offset] = null;
					reqsWriting[writeOffset] = item;
				} // else the request can happily stay where it is
				target++;
				if(excludeItem) {
					exclude++;
				} else {
					if(valid == random) { // Picked on previous round
						chosenItem = item;
					}
					if(validIndex == -1) {
						// Take the first valid item
						validIndex = target-1;
						validItem = item;
					}
					valid++;
				}
			}
			if(index != target) {
				index = target;
			}
			// We reach this point if 1) the random number we picked last round is invalid because an item became cancelled or excluded
			// or 2) we are on the first round anyway.
			if(chosenItem != null) {
				ret = chosenItem;
				if(logMINOR) Logger.minor(this, "Chosen random item "+ret+" out of "+valid+" total "+index);
				return new RemoveRandomReturn(ret);
			}
			if(valid == 0 && exclude == 0) {
				if(logMINOR) Logger.minor(this, "No valid or excluded items total "+index);
				return null; // Caller should remove the whole RGA
			} else if(valid == 0) {
				if(logMINOR) Logger.minor(this, "No valid items, "+exclude+" excluded items total "+index);
				setWakeupTime(wakeupTime, context);
				return new RemoveRandomReturn(wakeupTime);
			} else if(valid == 1) {
				ret = validItem;
				if(logMINOR) Logger.minor(this, "No valid or excluded items apart from "+ret+" total "+index);
				return new RemoveRandomReturn(ret);
			} else {
				random = context.fastWeakRandom.nextInt(valid);
				if(logMINOR) Logger.minor(this, "Looping to choose valid item "+random+" of "+valid+" (excluded "+exclude+")");
				// Loop
			}
		}
	}

	/**
	 * blockNo is assumed to be already active. The last block is assumed not 
	 * to be.
	 */
	private void remove(int blockNo, int i) {
		index--;
		int endBlock = index / BLOCK_SIZE;
		if(blocks.length == 1 || blockNo == endBlock) {
			RandomGrabArrayItem[] items = blocks[blockNo].reqs;
			int idx = index % BLOCK_SIZE;
			items[i % BLOCK_SIZE] = items[idx];
			items[idx] = null;
		} else {
			RandomGrabArrayItem[] toItems = blocks[blockNo].reqs;
			RandomGrabArrayItem[] endItems = blocks[endBlock].reqs;
			toItems[i % BLOCK_SIZE] = endItems[index % BLOCK_SIZE];
			endItems[index % BLOCK_SIZE] = null;
		}
	}

	public void remove(RandomGrabArrayItem it, ClientContext context) {
		if(logMINOR)
			Logger.minor(this, "Removing "+it+" from "+this);
		
		boolean matched = false;
		boolean empty = false;
		synchronized(root) {
			if(blocks.length == 1) {
				Block block = blocks[0];
				for(int i=0;i<index;i++) {
					if(block.reqs[i] == it) {
						block.reqs[i] = block.reqs[--index];
						block.reqs[index] = null;
						matched = true;
						break;
					}
				}
				if(index == 0) empty = true;
			} else {
				int x = 0;
				for(int i=0;i<blocks.length;i++) {
					Block block = blocks[i];
					for(int j=0;j<block.reqs.length;j++) {
						if(x >= index) break;
						x++;
						if(block.reqs[j] == it) {
							int pullFrom = --index;
							int idx = pullFrom % BLOCK_SIZE;
							int endBlock = pullFrom / BLOCK_SIZE;
							if(i == endBlock) {
								block.reqs[j] = block.reqs[idx];
								block.reqs[idx] = null;
							} else {
								Block fromBlock = blocks[endBlock];
								block.reqs[j] = fromBlock.reqs[idx];
								fromBlock.reqs[idx] = null;
							}
							matched = true;
							break;
						}
					}
				}
				if(index == 0) empty = true;
			}
		}
		// Caller will typically clear it before calling for synchronization reasons.
		RandomGrabArray oldArray = it.getParentGrabArray();
		if(oldArray == this)
			it.setParentGrabArray(null);
		else if(oldArray != null)
			Logger.error(this, "Removing item "+it+" from "+this+" but RGA is "+it.getParentGrabArray(), new Exception("debug"));
		if(!matched) {
			if(logMINOR) Logger.minor(this, "Not found: "+it+" on "+this);
			return;
		}
		if(empty && parent != null) {
			parent.maybeRemove(this, context);
		}
	}

	public boolean isEmpty() {
	    synchronized(root) {
	        return index == 0;
	    }
	}
	
	public boolean contains(RandomGrabArrayItem item) {
		synchronized(root) {
			if(blocks.length == 1) {
				Block block = blocks[0];
				for(int i=0;i<index;i++) {
					if(block.reqs[i] == item) {
						return true;
					}
				}
			} else {
				int x = 0;
				for(int i=0;i<blocks.length;i++) {
					Block block = blocks[i];
					for(int j=0;j<block.reqs.length;j++) {
						if(x >= index) break;
						x++;
						if(block.reqs[i] == item) {
							return true;
						}
					}
				}
			}
		}
		return false;
	}
	
	public int size() {
	    synchronized(root) {
	        return index;
	    }
	}

	public RandomGrabArrayItem get(int idx) {
	    synchronized(root) {
	        int blockNo = idx / BLOCK_SIZE;
	        RandomGrabArrayItem item = blocks[blockNo].reqs[idx % BLOCK_SIZE];
	        return item;
	    }
	}
	
	// REDFLAG this method does not move cooldown items.
	// At present it is only called on startup so this is okay.
	public void moveElementsTo(RandomGrabArray existingGrabber,
			boolean canCommit) {
		WrapperManager.signalStarting((int) MINUTES.toMillis(5));
		for(Block block: blocks) {
			for(int j=0;j<block.reqs.length;j++) {
				RandomGrabArrayItem item = block.reqs[j];
				if(item == null) continue;
				item.setParentGrabArray(null);
				existingGrabber.add(item, null);
				block.reqs[j] = null;
			}
			System.out.println("Moved block in RGA "+this);
		}
	}

	@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;
        }
    }
    
    /** Set the wakeup time, and update parents recursively if it is reduced. If it is increased
     * we don't need to bother parents as they will recompute the next time they need to. Only
     * called by removeRandomExhaustive() i.e. after checking <b>all</b> our 
     * RandomGrabArrayItem's and finding that none of them are ready to send.
     * @param wakeupTime
     * @param context
     */
    private void setWakeupTime(long wakeupTime, ClientContext context) {
        if(logMINOR) Logger.minor(this, "setCooldownTime("+(wakeupTime-System.currentTimeMillis())+") on "+this);
        synchronized(root) {
            if(this.wakeupTime > wakeupTime) {
                this.wakeupTime = wakeupTime; // Set before calling parent.
                if(parent != null) parent.reduceWakeupTime(wakeupTime, context);
            } else {
                this.wakeupTime = wakeupTime;
            }
        }
    }

    @Override
    public boolean reduceWakeupTime(long wakeupTime, ClientContext context) {
        if(logMINOR) Logger.minor(this, "reduceCooldownTime("+(wakeupTime-System.currentTimeMillis())+") on "+this);
        synchronized(root) {
            if(this.wakeupTime > wakeupTime) {
                this.wakeupTime = wakeupTime;
                if(parent != null) parent.reduceWakeupTime(wakeupTime, context);
                return true;
            }
            return false;
        }
    }

    @Override
    public void clearWakeupTime(ClientContext context) {
        if(logMINOR) Logger.minor(this, "clearCooldownTime() on "+this);
        synchronized(root) {
            wakeupTime = 0;
            if(parent != null) parent.clearWakeupTime(context);
        }
    }
	
}
