package freenet.clients.fcp;

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

import freenet.client.ClientMetadata;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.InsertContext;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.client.async.CacheFetchResult;
import freenet.client.events.SplitfileProgressEvent;
import freenet.clients.fcp.ClientPut.COMPRESS_STATE;
import freenet.keys.FreenetURI;
import freenet.support.Logger;
import freenet.support.MultiValueTable;
import freenet.support.api.Bucket;
import freenet.support.io.NoFreeBucket;

/** Per-PersistentRequestClient cache of status of requests. */
public class RequestStatusCache {
	
    private static volatile boolean logMINOR;
    
	static {
		Logger.registerClass(RequestStatusCache.class);
	}

	private final ArrayList<RequestStatus> downloads;
	private final ArrayList<RequestStatus> uploads;
	private final Map<String, RequestStatus> requestsByIdentifier;
	private final MultiValueTable<FreenetURI, DownloadRequestStatus> downloadsByURI;
	private final MultiValueTable<FreenetURI, RequestStatus> uploadsByFinalURI;
	
	RequestStatusCache() {
		downloads = new ArrayList<>();
		uploads = new ArrayList<>();
		requestsByIdentifier = new HashMap<>();
		downloadsByURI = new MultiValueTable<>();
		uploadsByFinalURI = new MultiValueTable<>();
	}
	
	synchronized void addDownload(DownloadRequestStatus status) {
		RequestStatus old = 
			requestsByIdentifier.put(status.getIdentifier(), status);
		if(logMINOR) Logger.minor(this, "Starting download "+status.getIdentifier());
		if(old == status) return;
		if(old != null)
		    downloads.remove(old);
		downloads.add(status);
		downloadsByURI.put(status.getURI(), status);
	}
	
	synchronized void addUpload(UploadRequestStatus status) {
		RequestStatus old = 
			requestsByIdentifier.put(status.getIdentifier(), status);
		if(old == status) return;
		if(logMINOR) Logger.minor(this, "Starting upload "+status.getIdentifier());
		if(old != null)
		    uploads.remove(old);
		uploads.add(status);
		FreenetURI uri = status.getURI();
		if(uri != null)
			uploadsByFinalURI.put(uri, status);
	}
	
	synchronized void finishedDownload(String identifier, boolean success, long dataSize, 
			String mimeType, FetchExceptionMode failureCode, String failureReasonLong, String failureReasonShort, Bucket dataShadow, boolean filtered) {
		DownloadRequestStatus status = (DownloadRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.setFinished(success, dataSize, mimeType, failureCode, failureReasonLong,
				failureReasonShort, dataShadow, filtered);
	}
	
	synchronized void gotFinalURI(String identifier, FreenetURI finalURI) {
		UploadRequestStatus status = (UploadRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		if(status.getFinalURI() == null)
			// No final URI set yet, put into the index.
			uploadsByFinalURI.put(finalURI, status);
		status.setFinalURI(finalURI);
	}
	
	synchronized void finishedUpload(String identifier, boolean success,  
			FreenetURI finalURI, InsertExceptionMode failureCode, String failureReasonShort, 
			String failureReasonLong) {
		UploadRequestStatus status = (UploadRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		if(status.getFinalURI() == null && finalURI != null)
			// No final URI set yet, put into the index.
			uploadsByFinalURI.put(finalURI, status);
		status.setFinished(success, finalURI, failureCode, failureReasonShort, failureReasonLong);
	}
	
	synchronized void updateStatus(String identifier, SplitfileProgressEvent event) {
		RequestStatus status = requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.updateStatus(event);
	}
	
	synchronized void updateDetectedCompatModes(String identifier, InsertContext.CompatibilityMode[] compatModes, byte[] splitfileKey, boolean dontCompress) {
		DownloadRequestStatus status = (DownloadRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.updateDetectedCompatModes(compatModes, dontCompress);
		status.updateDetectedSplitfileKey(splitfileKey);
	}
	
	synchronized void removeByIdentifier(String identifier) {
		RequestStatus status = requestsByIdentifier.remove(identifier);
		if(status == null) return;
		if(status instanceof DownloadRequestStatus) {
			downloads.remove(status);
			FreenetURI uri = status.getURI();
			assert(uri != null);
			downloadsByURI.removeElement(uri, (DownloadRequestStatus) status);
		} else if(status instanceof UploadRequestStatus) {
			uploads.remove(status);
			FreenetURI uri = ((UploadRequestStatus) status).getFinalURI();
			if(uri != null)
				uploadsByFinalURI.removeElement(uri, status);
		}
	}

	synchronized void clear() {
		downloads.clear();
		uploads.clear();
		requestsByIdentifier.clear();
		downloadsByURI.clear();
		uploadsByFinalURI.clear();
	}

	public void updateCompressionStatus(String identifier,
			COMPRESS_STATE compressing) {
		UploadFileRequestStatus status = (UploadFileRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.updateCompressionStatus(compressing);
	}

	public synchronized void addTo(List<RequestStatus> status) {
		// FIXME is it better to just synchronize on the RequestStatusCache when
		// rendering the downloads page, and when updating? Ugly though ...
		for(RequestStatus req : requestsByIdentifier.values())
			status.add(req.clone());
	}

	public synchronized void updateExpectedMIME(String identifier, String foundDataMimeType) {
		DownloadRequestStatus status = (DownloadRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.updateExpectedMIME(foundDataMimeType);
	}

	public synchronized void updateExpectedDataLength(String identifier, long expectedDataLength) {
		DownloadRequestStatus status = (DownloadRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.updateExpectedDataLength(expectedDataLength);
	}

	public void setPriority(String identifier, short newPriorityClass) {
		RequestStatus status = requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.setPriority(newPriorityClass);
	}
	
	/** Restart a request. Caller should call ,false first, at which point we setStarted,
	 * and ,true when it has actually started (a race condition means we don't setStarted
	 * at that point since it's possible the success/failure callback might happen first). */
	public synchronized void updateStarted(String identifier, boolean started) {
		RequestStatus status = requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		
		if(!started)
			// Caller should call with false first, so we only need to unset finished when setting started=false.
			status.restart(false);
		else
			// Already restarted, just set started = true.
			status.setStarted(started);
	}
	
	/** Restart a download. Caller should call ,false first, at which point we setStarted,
	 * and ,true when it has actually started (a race condition means we don't setStarted
	 * at that point since it's possible the success/failure callback might happen first).
	 * @param redirect If non-null, the request followed a redirect. */
	public synchronized void updateStarted(String identifier, FreenetURI redirect) {
		DownloadRequestStatus status = (DownloadRequestStatus) requestsByIdentifier.get(identifier);
		if(status == null) return; // Can happen during cancel etc.
		status.restart(false);
		if(redirect != null) {
			downloadsByURI.remove(status.getURI());
			status.redirect(redirect);
			downloadsByURI.put(redirect, status);
		}
	}

	public synchronized CacheFetchResult getShadowBucket(FreenetURI key, boolean noFilter) {
		for (DownloadRequestStatus download : downloadsByURI.getAllAsList(key)) {
			Bucket data = download.getDataShadow();
			if (data == null) {
				continue;
			}
			if (data.size() == 0) {
				continue;
			}
			if (noFilter && download.filterData) {
				continue;
			}
			// FIXME it probably *is* worth the effort to allow this when it is overridden on the fetcher, since the user changed the type???
			if (download.overriddenDataType) {
				continue;
			}
			return new CacheFetchResult(
				new ClientMetadata(download.getMIMEType()),
				new NoFreeBucket(data),
				download.filterData
			);
		}
		return null;
	}

}
