package serverP2P;
import serverP2P.FileWatcher;
import serverP2P.RatioWatcher;
import tools.Logger;
import tools.LogLevel;
import tools.HostItem;
import tools.ServeErrors;
import protocolP2P.ProtocolP2PPacket;
import protocolP2P.Payload;
import protocolP2P.RequestResponseCode;
import protocolP2P.FileList;
import protocolP2P.FilePart;
import protocolP2P.LoadRequest;
import protocolP2P.HashRequest;
import protocolP2P.HashResponse;
import protocolP2P.HashAlgorithm;
import protocolP2P.Unregister;
import protocolP2P.SizeRequest;
import protocolP2P.SizeResponse;
import protocolP2P.Denied;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.io.File;
import java.util.Arrays;
import java.util.Map;
import java.util.HashMap;
import java.util.Random;
import java.io.IOException;
import exception.LocalException;
import localException.InternalError;
import remoteException.UnknownHost;

/** Implementation of P2P-JAVA-PROJECT VERSION 1.0 protocol.
 * @author	Louis Royer
 * @author	Flavien Haas
 * @author	JS Auge
 * @version	1.0
 */
public abstract class ServerManagement extends ServeErrors implements Runnable {

	protected volatile boolean stop;
	protected FileWatcher fileListWatcher;
	protected Logger logger;
	protected String baseDirectory;
	protected HostItem server;
	protected HostItem tracker;
	protected Random punisher = new Random();
	protected RatioWatcher ratioWatcher;

	/** Constructor */
	public ServerManagement(String baseDirectory, HostItem server, HostItem tracker, Logger logger) {
		super();
		assert baseDirectory != null : "baseDirectory is null";
		assert server != null : "server is null";
		assert tracker != null : "tracker is null";
		assert logger != null : "logger is null";
		stop = false;
		this.baseDirectory = baseDirectory;
		this.server = server;
		this.tracker = tracker;
		this.logger = logger;
	}

	/** Stop the thread */
	public void setStop() {
		stop = true;
		fileListWatcher.setStop();
		ratioWatcher.setStop();
		closeSocket();
		writeLog("Server stopped", LogLevel.Info);
	}

	/** Closes socket */
	protected abstract void closeSocket();

	/** Trigger a manual check of the file list
	 */
	public void updateFileList() {
		if (fileListWatcher != null) {
			fileListWatcher.trigger();
		}
	}

	/** Send response to list request
	 * @param pd	Request received
	 */
	protected < T extends ProtocolP2PPacket<?> > void sendListResponse(T pd) {
		try {
			String[] fileList = fileListWatcher.getFileList();
			if (fileList.length == 0) {
				writeLog("Sending EMPTY_DIRECTORY to host " + pd.getHostItem(), LogLevel.Action);
				sendEmptyDirectory(pd);
			} else {
				writeLog("Sending LIST_RESPONSE to host " + pd.getHostItem(), LogLevel.Action);
				pd.sendResponse(createProtocolP2PPacket((Payload)(new FileList(fileList))));
			}
		} catch (Exception e2) {
			writeLog(e2, LogLevel.Error);
		}
	}



	/** Send hash response to hash request
	 * @param pd	Request received
	 */
	protected < T extends ProtocolP2PPacket<?> > void sendHashResponse(T pd) {
		Payload p = pd.getPayload();
		assert p instanceof HashRequest : "payload must be an instance of HashRequest";
		if (!(p instanceof HashRequest)) {
			sendInternalError(pd);
		} else {
			String filename = ((HashRequest)p).getFilename();
			if (Arrays.binarySearch(fileListWatcher.getFileList(), filename) >= 0) {
				Map<HashAlgorithm, byte[]> hashes = new HashMap<>();
				for (HashAlgorithm h : ((HashRequest)p).getAlgoList()) {
					switch (h) {
						case SHA512:
							hashes.put(h, fileListWatcher.getSha512Map().get(filename));
							break;
						case MD5:
						default:
							hashes.put(h, new byte[0]);
							break;
					}
				}
				try {
					pd.sendResponse(createProtocolP2PPacket((Payload)(new HashResponse(filename, hashes))));
				} catch (Exception e) {
					writeLog(e, LogLevel.Error);
				}
			} else {
				// file not found
				try {
					sendNotFound(pd);
				} catch (Exception e) {
					writeLog(e, LogLevel.Error);
				}
			}
		}
	}

	/** Send response to load request
	 * @param pd	Request received
	 */
	protected < T extends ProtocolP2PPacket<?> > void sendLoadResponse(T pd) {
		Payload p = pd.getPayload();
		assert p instanceof LoadRequest : "payload must be an instance of LoadRequest";
		if (!(p instanceof LoadRequest)) {
			sendInternalError(pd);
		} else {
			String filename = ((LoadRequest)p).getFilename();
			long offset = ((LoadRequest)p).getOffset();
			long maxSizePartialContent = ((LoadRequest)p).getMaxSizePartialContent();
			try {
				String[] fileList = fileListWatcher.getFileList();
				if (Arrays.binarySearch(fileList, filename) >= 0) {
					try {
						double proba = ratioWatcher.getPunishmentProbability(((LoadRequest)p).getHostItem());
						if (punisher.nextDouble() <= proba) {
							writeLog("Sending punishment", LogLevel.Debug);
							pd.sendResponse(createProtocolP2PPacket(new Denied(filename, offset)));
						} else {
							byte[] fullLoad = Files.readAllBytes(Paths.get(baseDirectory + filename));
							long sizeToSend = 0;
							if (fullLoad.length - offset < maxSizePartialContent) {
								writeLog("Sending last partialContent", LogLevel.Debug);
								sizeToSend = fullLoad.length - offset;
							} else {
								sizeToSend = maxSizePartialContent;
							}
							writeLog("maxSizePartialContent: " + maxSizePartialContent, LogLevel.Debug);
							writeLog("Sending " + filename + " from " + offset + " to " + (offset + sizeToSend), LogLevel.Debug);
							byte[] load = Arrays.copyOfRange(fullLoad, (int)offset, (int)(offset + sizeToSend));
							try {
								if (load.length == 0) {
									sendEmptyFile(pd);
								} else {
									pd.sendResponse(createProtocolP2PPacket((Payload)(new FilePart(filename, offset, load))));
								}
							} catch (Exception e2) {
								writeLog(e2, LogLevel.Error);
							}
						}
					} catch (InternalError e) {
						writeLog("InternalError", LogLevel.Debug);
						writeLog(e, LogLevel.Debug);
						sendInternalError(pd);
						return;
					} catch (UnknownHost e) {
						writeLog("Unknown host: " + ((LoadRequest)p).getHostItem(), LogLevel.Debug);
						writeLog(e, LogLevel.Debug);
						sendInternalError(pd);
						return;
					} catch(LocalException e) {
						sendInternalError(pd);
						return;
					}
				} else {
					writeLog("File requested not found: `" + filename + "` " + Arrays.binarySearch(fileList, filename), LogLevel.Debug);
					writeLog("File list:", LogLevel.Debug);
					for (String f: fileList) {
						writeLog("- " + f, LogLevel.Debug);
					}

					throw new IOException(); // to send a NOT_FOUND in the catch block
				}
			} catch (IOException e) {
				try {
					sendNotFound(pd);
				} catch (Exception e2) {
					writeLog(e2, LogLevel.Debug);
				}
			}
		}
	}

	/** Send response to size request
	 * @param pd	Request received
	 */
	protected < T extends ProtocolP2PPacket<?> > void sendSizeResponse(T pd) {
		Payload p = pd.getPayload();
		assert p instanceof SizeRequest : "payload must be an instance of SizeRequest";
		if (!(p instanceof SizeRequest)) {
			sendInternalError(pd);
		} else {
			String filename = ((SizeRequest)p).getFilename();
			try {
				long size = (new File(baseDirectory + filename)).length();
				String[] fileList = fileListWatcher.getFileList();
				if (Arrays.binarySearch(fileList, filename) >= 0) {
					try {
						if (size == 0) {
							sendEmptyFile(pd);
						} else {
							pd.sendResponse(createProtocolP2PPacket((new SizeResponse(filename, size))));
						}
					} catch (Exception e2) {
						writeLog(e2, LogLevel.Error);
					}
				} else {
					throw new IOException(); // to send a NOT_FOUND in the catch block
				}
			} catch (IOException e) {
				try {
					sendNotFound(pd);
				} catch (Exception e2) {
					writeLog(e2, LogLevel.Debug);
				}
			}
		}
	}


	/** Getter for tracker socket
	 */
	protected abstract Object getTrackerSocket(); 

	/** Send unregister request to tracker
	 */
	 protected void sendUnregisterRequest() {
		// unregistering from tracker
		try {
			writeLog("Unregistering from tracker", LogLevel.Info);
			createProtocolP2PPacket(new Unregister(server)).sendRequest(getTrackerSocket());
		} catch (Exception e) {
			writeLog("Cannot unregister from tracker", LogLevel.Error);
			writeLog(e, LogLevel.Error);
		}
	}

	/** Handle request.
	 * @throws	LocalException
	 */
	 protected < T extends ProtocolP2PPacket<?> > void handleRequest(T pd) throws LocalException {
		 Payload p = pd.getPayload();
		 switch (p.getRequestResponseCode()) {
			case LOAD_REQUEST:
				writeLog("Received LOAD_REQUEST from host " + pd.getHostItem(), LogLevel.Action);
				sendLoadResponse(pd);
				break;
			case SIZE_REQUEST:
				writeLog("Received SIZE_REQUEST from host " + pd.getHostItem(), LogLevel.Action);
				sendSizeResponse(pd);
				break;
			case LIST_REQUEST:
				writeLog("Received LIST_REQUEST from host " + pd.getHostItem(), LogLevel.Action);
				sendListResponse(pd);
				break;
			case HASH_REQUEST:
				writeLog("Received HASH_REQUEST from host " + pd.getHostItem(), LogLevel.Action);
				sendHashResponse(pd);
				break;
			case DISCOVER_REQUEST:
				writeLog("Received DISCOVER_REQUEST from host " + pd.getHostItem(), LogLevel.Action);
				sendNotATracker(pd);
				break;
			case UNREGISTER:
				writeLog("Received UNREGISTER from host " + pd.getHostItem(), LogLevel.Action);
				sendNotATracker(pd);
				break;
			case REGISTER:
				writeLog("Received REGISTER from host " + pd.getHostItem(), LogLevel.Action);
				sendNotATracker(pd);
				break;
			default:
				writeLog("Received grabbage from host " + pd.getHostItem(), LogLevel.Action);
				sendInternalError(pd);
		}
	}

}
