package tracker;
import tools.ServeErrors;
import tools.HostItem;
import tools.Logger;
import tools.LogLevel;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import protocolP2P.ProtocolP2PPacket;
import protocolP2P.Payload;
import protocolP2P.DiscoverRequest;
import protocolP2P.DiscoverResponse;
import protocolP2P.FileList;
import protocolP2P.Unregister;
import protocolP2P.Register;
import protocolP2P.RequestResponseCode;
import protocolP2P.RatioRequest;
import protocolP2P.RatioResponse;
import protocolP2P.UpdateRatio;
import localException.InternalError;
import remoteException.EmptyDirectory;
import exception.LocalException;


/** Tracker management implementation
 * @author	Louis Royer
 * @author	Flavien Haas
 * @author	JS Auge
 * @version	1.0
 */
public abstract class TrackerManagement extends ServeErrors implements Runnable {
	protected HostItem tracker;
	protected Logger logger;
	protected List<HostItem> hostList = new ArrayList<>();
	protected Map<HostItem, Long> ratioUp = new HashMap<>();
	protected Map<HostItem, Long> ratioDown = new HashMap<>();
	protected Map<String, List<HostItem>> fileList = new HashMap<>();
	protected volatile boolean stop;

	/** Constructor
	 * @param tracker	Tracker HostItem
	 * @param logger	Logger
	 */
	public TrackerManagement(HostItem tracker, Logger logger) {
		stop = false;
		this.tracker = tracker;
		this.logger = logger;
	}

	/** Handle Discover request
	 * @param pd	Received request
	 * @throws	InternalError
	 */
	protected < T extends ProtocolP2PPacket<?> > void handleDiscover(T pd) throws InternalError {
		Payload p = pd.getPayload();
		assert p instanceof DiscoverRequest : "payload must be an instance of DiscoverRequest";
		if (!(p instanceof DiscoverRequest)) {
			sendInternalError(pd);
		} else {
			String filename = ((DiscoverRequest)p).getFilename();
			try {
				pd.sendResponse(createProtocolP2PPacket(new DiscoverResponse(filename, fileList.getOrDefault(filename, hostList))));
			} catch (Exception e) {
				writeLog(e, LogLevel.Error);
			}
		}
	}


	/** Handle Ratio request
	 * @param pd	Received request
	 * @throws	InternalError
	 */
	protected < T extends ProtocolP2PPacket<?> > void handleRatio(T pd) throws InternalError {
		Payload p = pd.getPayload();
		assert p instanceof RatioRequest : "payload must be an instance of RatioRequest";
		if (!(p instanceof RatioRequest)) {
			sendInternalError(pd);
		} else {
			HostItem host = ((RatioRequest)p).getHostItem();
			writeLog("Ratio request for host " + host, LogLevel.Debug);
			try {
				if (!hostList.contains(host)) {
					String l = "";
					for (HostItem h: hostList) {
						l += h + " ";
					}
					writeLog(host + " is not in hostlist [ " + l + "]", LogLevel.Debug);
					pd.sendResponse(createProtocolP2PPacket(new Payload(RequestResponseCode.UNKNOWN_HOST)));
				} else {
					pd.sendResponse(createProtocolP2PPacket(new RatioResponse(host,
						ratioUp.get(host).longValue(),
						ratioDown.get(host).longValue())));
				}
			} catch (Exception e) {
				writeLog(e, LogLevel.Error);
			}
		}
	}

	/** Handle List Responses
	 * @param pd	Received response
	 * @throws	InternalError
	 */
	protected <T extends ProtocolP2PPacket<?> > void handleListResponse(T pd, HostItem host) throws InternalError {
		Payload p = pd.getPayload();
		assert p instanceof FileList: "payload must be an instance of FileList";
		if (!(p instanceof FileList)) {
			throw new InternalError();
		} else {
			String[] f = ((FileList)p).getFileList();
			for (String file: f) {
				List<HostItem> h = fileList.get(file);
				if (h != null) {
					if (!h.contains(host)) {
						h.add(host);
					}
				} else {
					List<HostItem> emptyH = new ArrayList<>();
					emptyH.add(host);
					fileList.put(file, emptyH);
				} 
			}
		}
	}

	/** Handle Unregistering
	 * @param pd	Request received
	 * @throws	InternalError
	 */
	protected < T extends ProtocolP2PPacket<?> > void handleUnregister(T pd) throws InternalError {
		Payload p = pd.getPayload();
		assert p instanceof Unregister : "payload must be an instance of Unregister";
		if (!(p instanceof Unregister)) {
			sendInternalError(pd);
			throw new InternalError();
		}
		HostItem host = ((Unregister)p).getHostItem();
		writeLog("Received UNREGISTER from host " + pd.getHostItem() + ". Removing host " + host, LogLevel.Action);
		hostList.remove(host);
		for(String f: fileList.keySet()) {
			fileList.get(f).remove(host);
			if(fileList.get(f).isEmpty()) {
				fileList.remove(f);
			}
		}

		/* Note: we do not remove host from ratioUp and ratioDown since Unregistering is only here to indicate
		 * the host is temporary not serving any files (ie. is down).
		 */ 
	}

	/** Getter for HostItem socket
	 * @param hostItem	HostItem
	 */
	protected abstract Object getHostItemSocket(HostItem hostItem);

	/** Close HostItem socket
	 * @param hostItem	HostItem
	 */
	protected abstract void closeHostItemSocket(HostItem hostItem);

	/** Handle Update Ratio
	 * @param pd	Received request
	 * @throws	InternalError
	 */
	protected < T extends ProtocolP2PPacket<?> > void handleUpdateRatio(T pd) throws InternalError {
		Payload p = pd.getPayload();
		assert p instanceof UpdateRatio : "payload must be an instance of UpdateRatio";
		if (!(p instanceof UpdateRatio)) {
			throw new InternalError();
		}

		UpdateRatio updateRatio = (UpdateRatio) p;
		HostItem updateRatioServer = updateRatio.getServer();
		HostItem updateRatioClient = updateRatio.getClient();
		long ratioSize = updateRatio.getDataSize();
		writeLog("Ratio += " + ratioSize + ", client: " + updateRatioClient + " / server: " + updateRatioServer, LogLevel.Debug);
		if (!ratioDown.containsKey(updateRatioClient) || ! ratioUp.containsKey(updateRatioServer)) {
			sendUnknownHost(pd);
		} else {
			ratioDown.put(updateRatioClient, Long.valueOf(ratioDown.get(updateRatioClient).longValue() + ratioSize));
			ratioUp.put(updateRatioServer, Long.valueOf(ratioUp.get(updateRatioServer).longValue() + ratioSize));
		}
	}

	/** Handle Registering
	 * @param pd	Received request
	 * @throws	InternalError
	 */
	protected < T extends ProtocolP2PPacket<?> > void handleRegister(T pd) throws InternalError {
		Payload p = pd.getPayload();
		assert p instanceof Register : "payload must be an instance of Register";
		if (!(p instanceof Register)) {
			throw new InternalError();
		} 
		// add host to known host list
		HostItem host = ((Register)p).getHostItem();
		if (!hostList.contains(host)) {
			hostList.add(host);
		}

		// initialize ratios if this is a new host
		ratioUp.putIfAbsent(host, Long.valueOf(0));
		ratioDown.putIfAbsent(host, Long.valueOf(0));
		
		// send a list request
		try {
			ProtocolP2PPacket<?> pLReq = createProtocolP2PPacket(new Payload(RequestResponseCode.LIST_REQUEST));
			pLReq.sendRequest(getHostItemSocket(host));
			writeLog("Received REGISTER from host " + pd.getHostItem() + ". Adding host " + host + " to list. Sending List request", LogLevel.Action);
			handleListResponse(pLReq.receiveResponse(), host);
			writeLog("Received LIST RESPONSE from host " + pd.getHostItem(), LogLevel.Action);
			closeHostItemSocket(host);
		} catch (EmptyDirectory e) {
			writeLog("Empty Directory", LogLevel.Debug);
			hostList.remove(host);
			writeLog("Received EMPTY DIRECTORY from host " + pd.getHostItem() + ". Aborting.", LogLevel.Action);
		} catch (Exception e) {
			// remove from list because list request could not be send
			hostList.remove(host);
			writeLog("Aborting the add of host " + host, LogLevel.Action);
			writeLog(e, LogLevel.Error);
		}
	}


	/** Handle requests
	 * @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() + ", sending NOT_FOUND", LogLevel.Action);
				sendNotFound(pd);
				break;
			case LIST_REQUEST:
				writeLog("Received LIST_REQUEST from host " + pd.getHostItem() + ", sending EMPTY_DIRECTORY", LogLevel.Action);
				sendEmptyDirectory(pd);
				break;
			case HASH_REQUEST:
				writeLog("Received HASH_REQUEST from host " + pd.getHostItem() + ", sending NOT_FOUND", LogLevel.Action);
				sendNotFound(pd);
				break;
			case REGISTER:
				writeLog("Received REGISTER from host " + pd.getHostItem(), LogLevel.Debug);
				handleRegister(pd);
				break;
			case UNREGISTER:
				writeLog("Received UNREGISTER from host " + pd.getHostItem(), LogLevel.Debug);
				handleUnregister(pd);
				break;
			case DISCOVER_REQUEST:
				writeLog("Received DISCOVER REQUEST from host " + pd.getHostItem(), LogLevel.Action);
				handleDiscover(pd);
				break;
			case RATIO_REQUEST:
				writeLog("Received RATIO REQUEST from host " + pd.getHostItem(), LogLevel.Action);
				handleRatio(pd);
				break;
			case UPDATE_RATIO:
				writeLog("Received UPDATE RATIO from host " + pd.getHostItem(), LogLevel.Action);
				handleUpdateRatio(pd);
				break;
			default:
				writeLog("Received grabbage from host " + pd.getHostItem(), LogLevel.Action);
				sendInternalError(pd);
				break;
		}
	}

	/** Stop the thread */
	public void setStop() {
		stop = true;
	}

}
