package freenet.clients.fcp;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientRequester;
import freenet.clients.fcp.ClientRequest.Persistence;
import freenet.clients.fcp.ListPersistentRequestsMessage.PersistentListJob;
import freenet.clients.fcp.ListPersistentRequestsMessage.TransientListJob;
import freenet.keys.FreenetURI;
import freenet.node.RequestClient;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;

/**
 * An FCP client.
 * Identified by its Name which is sent on connection. 
 * Tracks persistent requests for either PERSISTENCE_REBOOT or PERSISTENCE_FOREVER.
 */
public class PersistentRequestClient {
	
	public PersistentRequestClient(String name2, FCPConnectionHandler handler, boolean isGlobalQueue, RequestCompletionCallback cb, Persistence persistence, PersistentRequestRoot root) {
		this.name = name2;
		if(name == null) throw new NullPointerException();
		this.currentConnection = handler;
		final boolean forever = (persistence == Persistence.FOREVER);
		runningPersistentRequests = new ArrayList<ClientRequest>();
		completedUnackedRequests = new ArrayList<ClientRequest>();
		clientRequestsByIdentifier = new HashMap<String, ClientRequest>();
		this.isGlobalQueue = isGlobalQueue;
		this.persistence = persistence;
		assert(persistence == Persistence.FOREVER || persistence == Persistence.REBOOT);
		watchGlobalVerbosityMask = Integer.MAX_VALUE;
		lowLevelClient = new FCPClientRequestClient(this, forever, false);
		lowLevelClientRT = new FCPClientRequestClient(this, forever, true);
		completionCallbacks = new ArrayList<RequestCompletionCallback>();
		if(cb != null) completionCallbacks.add(cb);
		if(persistence == Persistence.FOREVER) {
			assert(root != null);
			this.root = root;
		} else
			this.root = null;
		if(isGlobalQueue)
		    statusCache = new RequestStatusCache();
		else
		    statusCache = null;
	}
	
	/** The persistent root object, null if persistence is PERSIST_REBOOT */
	final PersistentRequestRoot root;
	/** The client's Name sent in the ClientHello message */
	final String name;
	/** The current connection handler, if any. */
	private transient FCPConnectionHandler currentConnection;
	/** Currently running persistent requests */
	private final List<ClientRequest> runningPersistentRequests;
	/** Completed unacknowledged persistent requests */
	private final List<ClientRequest> completedUnackedRequests;
	/** ClientRequest's by identifier */
	private final Map<String, ClientRequest> clientRequestsByIdentifier;
	/** Are we the global queue? */
	public final boolean isGlobalQueue;
	/** Are we watching the global queue? */
	transient boolean watchGlobal;
	transient int watchGlobalVerbosityMask;
	/** FCPClients watching us. Lazy init, sync on clientsWatchingLock */
	private transient LinkedList<PersistentRequestClient> clientsWatching;
	private final Object clientsWatchingLock = new Object();
	private RequestClient lowLevelClient;
	private RequestClient lowLevelClientRT;
	private transient List<RequestCompletionCallback> completionCallbacks;
	/** The cache where ClientRequests report their progress */
	private transient final RequestStatusCache statusCache;
	/** Connection mode */
	final Persistence persistence;
	        
        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	public synchronized FCPConnectionHandler getConnection() {
		return currentConnection;
	}
	
	public synchronized void setConnection(FCPConnectionHandler handler) {
		this.currentConnection = handler;
	}

	public synchronized void onLostConnection(FCPConnectionHandler handler) {
		handler.freeDDAJobs();
		if(currentConnection == handler)
			currentConnection = null;
	}

	/**
	 * Called when a client request has finished, but is persistent. It has not been
	 * acked yet, so it should be moved to the unacked-completed-requests set.
	 */
	public void finishedClientRequest(ClientRequest get) {
		if(logMINOR)
			Logger.minor(this, "Finished client request", new Exception("debug"));
		assert(get.persistence == persistence);
		synchronized(this) {
			if(runningPersistentRequests.remove(get)) {
				completedUnackedRequests.add(get);
			}	
		}
		if(statusCache != null) {
			if(get instanceof ClientGet) {
				ClientGet download = (ClientGet)get;
				GetFailedMessage msg = download.getFailureMessage();
				FetchExceptionMode failureCode = null;
				String shortFailMessage = null;
				String longFailMessage = null;
				if(msg != null) {
					failureCode = msg.code;
					shortFailMessage = msg.getShortFailedMessage();
					longFailMessage = msg.getLongFailedMessage();
				}
				Bucket shadow = ((ClientGet) get).getBucket();
				if(shadow != null) shadow = shadow.createShadow();
				statusCache.finishedDownload(get.identifier, get.hasSucceeded(), ((ClientGet) get).getDataSize(), ((ClientGet) get).getMIMEType(), failureCode, longFailMessage, shortFailMessage, shadow, download.filterData());
			} else if(get instanceof ClientPutBase) {
				ClientPutBase upload = (ClientPutBase)get;
				PutFailedMessage msg = upload.getFailureMessage();
				InsertExceptionMode failureCode = null;
				String shortFailMessage = null;
				String longFailMessage = null;
				if(msg != null) {
					failureCode = msg.code;
					shortFailMessage = msg.getShortFailedMessage();
					longFailMessage = msg.getLongFailedMessage();
				}
				statusCache.finishedUpload(upload.getIdentifier(), upload.hasSucceeded(), upload.getGeneratedURI(), failureCode, shortFailMessage, longFailMessage);
			} else assert(false);
		}
	}

	public void queuePendingMessagesOnConnectionRestartAsync(FCPConnectionOutputHandler outputHandler, ClientContext context) {
		if(persistence == Persistence.FOREVER) {
			PersistentListJob job = new PersistentListJob(this, outputHandler, context, null) {

				@Override
				void complete(ClientContext context) {
					// Do nothing.
				}
				
			};
			job.run(context);
		} else {
			TransientListJob job = new TransientListJob(this, outputHandler, context, null) {

				@Override
				void complete(ClientContext context) {
					// Do nothing.
				}
				
			};
			job.run(context);

		}
	}
	
	
	
	/**
	 * Queue any and all pending messages from already completed, unacknowledged, persistent
	 * requests, to be immediately sent. This happens automatically on startup and hopefully
	 * will encourage clients to acknowledge persistent requests!
	 */
	public int queuePendingMessagesOnConnectionRestart(FCPConnectionOutputHandler outputHandler, String listRequestIdentifier, int offset, int max) {
		Object[] reqs;
		synchronized(this) {
			reqs = completedUnackedRequests.toArray();
		}
		int i = 0;
		for(i=offset;i<Math.min(reqs.length,offset+max);i++) {
			ClientRequest req = (ClientRequest) reqs[i];
			((ClientRequest)reqs[i]).sendPendingMessages(outputHandler, listRequestIdentifier, true, false, false);
		}
		return i;
	}
	
	/**
	 * Queue any and all pending messages from running requests. Happens on demand.
	 */
	public int queuePendingMessagesFromRunningRequests(FCPConnectionOutputHandler outputHandler, String listRequestIdentifier, int offset, int max) {
		Object[] reqs;
		synchronized(this) {
			reqs = runningPersistentRequests.toArray();
		}
		int i = 0;
		for(i=offset;i<Math.min(reqs.length,offset+max);i++) {
			ClientRequest req = (ClientRequest) reqs[i];
			req.sendPendingMessages(outputHandler, listRequestIdentifier, true, false, false);
		}
		return i;
	}
	
	public void register(ClientRequest cg) throws IdentifierCollisionException {
		assert(cg.persistence == persistence);
		if(logMINOR)
			Logger.minor(this, "Registering "+cg.getIdentifier());
		synchronized(this) {
			String ident = cg.getIdentifier();
			ClientRequest old = clientRequestsByIdentifier.get(ident);
			if((old != null) && (old != cg))
				throw new IdentifierCollisionException();
			if(cg.hasFinished()) {
				completedUnackedRequests.add(cg);
			} else {
				runningPersistentRequests.add(cg);
			}
			clientRequestsByIdentifier.put(ident, cg);
		}
		if(statusCache != null) {
			if(cg instanceof ClientGet) {
				statusCache.addDownload((DownloadRequestStatus)(cg.getStatus()));
			} else if(cg instanceof ClientPutBase) {
				statusCache.addUpload((UploadRequestStatus)(cg.getStatus()));
			}
		}
	}

	public boolean removeByIdentifier(String identifier, boolean kill, FCPServer server, ClientContext context) {
		ClientRequest req;
		if(logMINOR) Logger.minor(this, "removeByIdentifier("+identifier+ ',' +kill+ ')');
		if(statusCache != null)
			statusCache.removeByIdentifier(identifier);
		synchronized(this) {
			req = clientRequestsByIdentifier.get(identifier);
//			if(container != null && req != null)
//				container.activate(req, 1);
			boolean removedFromRunning = false;
			if(req == null) {
				for(ClientRequest r : completedUnackedRequests) {
					if(r.getIdentifier().equals(identifier)) {
						req = r;
						completedUnackedRequests.remove(r);
						Logger.error(this, "Found completed unacked request "+r+" for identifier "+r.getIdentifier()+" but not in clientRequestsByIdentifier!!");
						break;
					}
				}
				if(req == null) {
					for(ClientRequest r : runningPersistentRequests) {
						if(r.getIdentifier().equals(identifier)) {
							req = r;
							runningPersistentRequests.remove(r);
							removedFromRunning = true;
							Logger.error(this, "Found running request "+r+" for identifier "+r.getIdentifier()+" but not in clientRequestsByIdentifier!!");
							break;
						}
					}
				}
				if(req == null) return false;
			} else if(!((removedFromRunning = runningPersistentRequests.remove(req)) || completedUnackedRequests.remove(req))) {
				Logger.error(this, "Removing "+identifier+": in clientRequestsByIdentifier but not in running/completed maps!");
				
				return false;
			}
			clientRequestsByIdentifier.remove(identifier);
		}
		if(kill) {
			if(logMINOR) Logger.minor(this, "Killing request "+req);
			req.cancel(context);
		}
        req.requestWasRemoved(context);
        RequestCompletionCallback[] callbacks = null;
        synchronized(this) {
        	if(completionCallbacks != null)
        		callbacks = completionCallbacks.toArray(new RequestCompletionCallback[completionCallbacks.size()]);
        }
		if(callbacks != null) {
			for(RequestCompletionCallback cb : callbacks)
				cb.onRemove(req);
		}
		return true;
	}

	public boolean hasPersistentRequests() {
		return !(runningPersistentRequests.isEmpty() && completedUnackedRequests.isEmpty());
	}

	public void addPersistentRequests(List<ClientRequest> v, boolean onlyForever) {
		synchronized(this) {
			for(ClientRequest req: runningPersistentRequests) {
				if(req == null) {
					Logger.error(this, "Request is null on runningPersistentRequests for "+this+" - database corruption??");
					continue;
				}
				if((req.isPersistentForever()) || !onlyForever)
					v.add(req);
			}
			v.addAll(completedUnackedRequests);
		}
	}
	
	/** From database */
	private void addPersistentRequestStatus(List<RequestStatus> status, boolean onlyForever) {
		// FIXME OPT merge with addPersistentRequests? Locking looks tricky.
		List<ClientRequest> reqs = new ArrayList<ClientRequest>();
		addPersistentRequests(reqs, onlyForever);
		for(ClientRequest req : reqs) {
			try {
				status.add(req.getStatus());
			} catch (Throwable t) {
				// Try to load the rest. :<
				Logger.error(this, "BROKEN REQUEST LOADING PERSISTENT REQUEST STATUS: "+t, t);
				// FIXME tell the user in wrapper.log or even in a useralert.
			}
			// FIXME deactivate? Unconditional deactivate depends on callers. Keep-as-is would need merge with addPersistentRequests.
		}
	}
	
	/** From cache */
	public void addPersistentRequestStatus(List<RequestStatus> status) {
		statusCache.addTo(status);
	}

	/**
	 * Enable or disable watch-the-global-queue.
	 * @param enabled Whether we want watch-global-queue to be enabled.
	 * @param verbosityMask If so, what verbosity mask to use (to filter messages
	 * generated by the global queue).
	 */
	public boolean setWatchGlobal(boolean enabled, int verbosityMask, FCPServer server) {
		if(isGlobalQueue) {
			Logger.error(this, "Set watch global on global queue!: "+this, new Exception("debug"));
			return false;
		}
		if(server.globalForeverClient == null) return false;
		if(watchGlobal && !enabled) {
			server.globalRebootClient.unwatch(this);
			server.globalForeverClient.unwatch(this);
			watchGlobal = false;
		} else if(enabled && !watchGlobal) {
			server.globalRebootClient.watch(this);
			server.globalForeverClient.watch(this);
			FCPConnectionHandler connHandler = getConnection();
			if(connHandler != null) {
				if(persistence == Persistence.REBOOT)
					server.globalRebootClient.queuePendingMessagesOnConnectionRestartAsync(connHandler.outputHandler, server.core.clientContext);
				else
					server.globalForeverClient.queuePendingMessagesOnConnectionRestartAsync(connHandler.outputHandler, server.core.clientContext);
			}
			watchGlobal = true;
		}
		// Otherwise the status is unchanged.
		this.watchGlobalVerbosityMask = verbosityMask;
		return true;
	}

	public void queueClientRequestMessage(FCPMessage msg, int verbosityLevel) {
		queueClientRequestMessage(msg, verbosityLevel, false);
	}
	
	public void queueClientRequestMessage(FCPMessage msg, int verbosityLevel, boolean useGlobalMask) {
		if(useGlobalMask && (verbosityLevel & watchGlobalVerbosityMask) != verbosityLevel)
			return;
		FCPConnectionHandler conn = getConnection();
		if(conn != null) {
			conn.outputHandler.queue(msg);
		}
		PersistentRequestClient[] clients;
		if(isGlobalQueue) {
			synchronized(clientsWatchingLock) {
				if(clientsWatching != null)
				clients = clientsWatching.toArray(new PersistentRequestClient[clientsWatching.size()]);
				else
					clients = null;
			}
			if(clients != null)
			for(PersistentRequestClient client: clients) {
				if(client.persistence != persistence) continue;
				client.queueClientRequestMessage(msg, verbosityLevel, true);
			}
		}
	}
	
	private void unwatch(PersistentRequestClient client) {
		if(!isGlobalQueue) return;
		synchronized(clientsWatchingLock) {
			if(clientsWatching != null)
			clientsWatching.remove(client);
		}
	}

	private void watch(PersistentRequestClient client) {
		if(!isGlobalQueue) return;
		synchronized(clientsWatchingLock) {
			if(clientsWatching == null)
				clientsWatching = new LinkedList<PersistentRequestClient>();
			clientsWatching.add(client);
		}
	}

	public synchronized ClientRequest getRequest(String identifier) {
		ClientRequest req = clientRequestsByIdentifier.get(identifier);
		return req;
	}

	@Override
	public String toString() {
		return super.toString()+ ':' +name;
	}

	/**
	 * Callback called when a request succeeds.
	 */
	public void notifySuccess(ClientRequest req) {
		assert(req.persistence == persistence);
        RequestCompletionCallback[] callbacks = null;
        synchronized(this) {
        	if(completionCallbacks != null)
        		callbacks = completionCallbacks.toArray(new RequestCompletionCallback[completionCallbacks.size()]);
        }
		if(callbacks != null) {
			for(RequestCompletionCallback cb : callbacks)
				cb.notifySuccess(req);
		}
	}

	/**
	 * Callback called when a request fails
	 * @param get
	 */
	public void notifyFailure(ClientRequest req) {
		assert(req.persistence == persistence);
        RequestCompletionCallback[] callbacks = null;
        synchronized(this) {
        	if(completionCallbacks != null)
        		callbacks = completionCallbacks.toArray(new RequestCompletionCallback[completionCallbacks.size()]);
        }
		if(callbacks != null) {
			for(RequestCompletionCallback cb : callbacks)
				cb.notifyFailure(req);
		}
	}
	
	public synchronized void addRequestCompletionCallback(RequestCompletionCallback cb) {
		if(completionCallbacks == null) completionCallbacks = new ArrayList<RequestCompletionCallback>(); // it is transient so it might be null
		completionCallbacks.add(cb);
	}
	
	public synchronized void removeRequestCompletionCallback(RequestCompletionCallback cb){
		if(completionCallbacks!=null) completionCallbacks.remove(cb);
	}

	public void removeAll(ClientContext context) {
		HashSet<ClientRequest> toKill = new HashSet<ClientRequest>();
		if(statusCache != null)
			statusCache.clear();
		synchronized(this) {
			for(ClientRequest req: runningPersistentRequests) {
				toKill.add(req);
			}
			runningPersistentRequests.clear();
			for(ClientRequest req : completedUnackedRequests) {
				toKill.add(req);
			}
			completedUnackedRequests.clear();
			for(ClientRequest req : clientRequestsByIdentifier.values()) {
				toKill.add(req);
			}
			clientRequestsByIdentifier.clear();
		}
	}

	public ClientGet getCompletedRequest(FreenetURI key) {
		// FIXME speed this up with another hashmap or something.
		// FIXME keep a transient hashmap in RAM, use it for fproxy.
		// FIXME consider supporting inserts too.
		for(int i=0;i<completedUnackedRequests.size();i++) {
			ClientRequest req = completedUnackedRequests.get(i);
			if(!(req instanceof ClientGet)) continue;
			ClientGet getter = (ClientGet) req;
			if(getter.getURI().equals(key)) {
				return getter;
			}
		}
		return null;
	}

	public RequestStatusCache getRequestStatusCache() {
		return statusCache;
	}
	
	public void updateRequestStatusCache() {
	    updateRequestStatusCache(statusCache);
	}
	
	private void updateRequestStatusCache(RequestStatusCache cache) {
		if(persistence == Persistence.FOREVER) {
			System.out.println("Loading cache of request statuses...");
			ArrayList<RequestStatus> statuses = new ArrayList<RequestStatus>();
			addPersistentRequestStatus(statuses, true);
			for(RequestStatus status : statuses) {
				if(status instanceof DownloadRequestStatus)
					cache.addDownload((DownloadRequestStatus)status);
				else
					cache.addUpload((UploadRequestStatus)status);
			}
		}
	}

	public RequestClient lowLevelClient(boolean realTime) {
		if(realTime)
			return lowLevelClientRT;
		else
			return lowLevelClient;
	}
	
    public void addPersistentRequesters(List<ClientRequester> requesters) {
        for(ClientRequest req : runningPersistentRequests)
            requesters.add(req.getClientRequest());
        for(ClientRequest req : completedUnackedRequests)
            requesters.add(req.getClientRequest());
    }

    public void resume(ClientRequest clientRequest) {
        if(clientRequest.hasFinished())
            completedUnackedRequests.add(clientRequest);
        else
            runningPersistentRequests.add(clientRequest);
        String identifier = clientRequest.identifier;
        if(clientRequestsByIdentifier.get(identifier) != null) {
            if(clientRequest != clientRequestsByIdentifier.get(identifier))
                throw new IllegalArgumentException("Adding new client request "+clientRequest+
                        " with same name \""+identifier+"\" as "+
                        clientRequestsByIdentifier.get(identifier));
            else {
                Logger.error(this, "Adding the same identifier twice: "+identifier);
                return;
            }
        } else {
            clientRequestsByIdentifier.put(identifier, clientRequest);
        }
    }

}
