package serverP2P;
import java.util.Vector;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.net.ServerSocket;
import java.net.Socket;
import protocolP2P.ProtocolP2PPacketTCP;
import protocolP2P.ProtocolP2PPacket;
import protocolP2P.RequestResponseCode;
import protocolP2P.Payload;
import protocolP2P.LoadRequest;
import protocolP2P.FileList;
import protocolP2P.FilePart;
import exception.InternalError;
import exception.ProtocolError;
import exception.SizeError;
import exception.TransmissionError;
import exception.VersionError;
import exception.SocketClosed;
import remoteException.EmptyDirectory;
import remoteException.InternalRemoteError;
import remoteException.NotFound;
import remoteException.ProtocolRemoteError;
import remoteException.VersionRemoteError;
import remoteException.EmptyFile;
import java.util.Arrays;
import tools.Logger;
import tools.LogLevel;
import java.util.HashMap;
import java.util.Map;
import protocolP2P.HashAlgorithm;
import protocolP2P.HashRequest;
import protocolP2P.HashResponse;


/** Implementation of P2P-JAVA-PROJECT VERSION 1.0 protocol for TCP.
 * @author	Louis Royer
 * @author	Flavien Haas
 * @author	JS Auge
 * @version	1.0
 */
public class ServerManagementTCP implements Runnable {

	private String[] fileList;
	private Map<String, byte[]> sha512 = new HashMap<>();
	private String baseDirectory;
	private int TCPPort;
	private ServerSocket socket;
	private Logger logger;

	/** Constructor for TCP implementation, with baseDirectory and TCPPort parameters.
	 * @param baseDirectory	the root directory where files are stored
	 * @param TCPPort	the server will listen on this port
	 */
	public ServerManagementTCP(String baseDirectory, int TCPPort, Logger logger) {
		this.logger = logger;
		this.baseDirectory = baseDirectory;
		this.TCPPort = TCPPort;
		initFileList();
		initSha512();
		try {
			socket = new ServerSocket(TCPPort);
		} catch (SocketException e) {
			logger.writeTCP("Error: cannot listen on port " + TCPPort, LogLevel.Error);
			System.exit(-1);
		} catch (IOException e) {
			logger.writeTCP("Error: cannot openning socket", LogLevel.Error);
			System.exit(-2);
		}
	}

	/** Implementation of runnable. This methods allows to run the server.
	 */
	public void run() {
		logger.writeTCP("Server sucessfully started", LogLevel.Info);
		do {
			try {
				Socket s = socket.accept();
				ClientHandler c = new ClientHandler(s);
				(new Thread(c)).start();
			} catch (IOException e) {
				logger.writeTCP("Error while accepting new connection", LogLevel.Warning);
			}
		} while(true);
	}

	/** Private runnable class allowing to serve one client.
	 */
	private class ClientHandler implements Runnable {
		private Socket s;
		private String addr;
		/** Constructor with a socket.
		 * @param	s	Socket of this client
		 */
		public ClientHandler(Socket s) {
			this.s = s;
			this.addr = "[" +s.getInetAddress().getHostAddress() + "]:" + s.getPort() + " ";
		}

		/** Implementation of runnable. This method allow to serve one client.
		 */
		public void run() {
			
			boolean end = false;
			logger.writeTCP(addr + "New connection", LogLevel.Action);
			do {
				end = handleRequest();
			} while(!end);
			logger.writeTCP(addr + "End of connection", LogLevel.Action);
		}

		/** Respond to next request incomming on socket s.
		 * @param s	Socket used to read request and send response
		 * @return	true if cannot expect another request (ie, socket is closed)
		 */
		private boolean handleRequest() {
			try {
				ProtocolP2PPacketTCP pd = new ProtocolP2PPacketTCP((Object)s);
				Payload p = pd.getPayload();
				switch (p.getRequestResponseCode()) {
					case LOAD_REQUEST:
						logger.writeTCP(addr + "LOAD_REQUEST", LogLevel.Action);
						sendLoadResponse(pd);
						break;
					case LIST_REQUEST:
						logger.writeTCP(addr + "LIST_REQUEST", LogLevel.Action);
						sendListResponse(pd);
						break;
					case HASH_REQUEST:
						logger.writeTCP(addr + "HASH_REQUEST", LogLevel.Action);
						sendHashResponse(pd);
						break;
					default:
						logger.writeTCP(addr + "Received grabbage", LogLevel.Action);
						sendInternalError(pd);
				}
			} catch (IOException e) {
				return true;
			} catch (SocketClosed e) {
				return true;
			}
			catch (TransmissionError e) {}
			catch (ProtocolError e) {}
			catch (VersionError e) {}
			catch (InternalError e) {}
			catch (SizeError e) {}
			return false;
		}
	}

	/** Initialize local list of all files allowed to be shared.
	 */
	private void initFileList() {
		File folder = new File(baseDirectory);
		Vector<String> v = new Vector<String>();
		File[] files = folder.listFiles();
		/* Add non-recursively files's names to fileList */
		for (File f : files) {
			if (f.isFile()) {
				v.add(f.getName());
			}
		}
		fileList = new String[v.size()];
		v.toArray(fileList);
		Arrays.sort(fileList);
	}

	/** Init sha512 map.
	 */
	 private void initSha512() {
		for(String f: fileList) {
			try {
				MessageDigest md = MessageDigest.getInstance(HashAlgorithm.SHA512.getName()); 
				sha512.put(f, md.digest(Files.readAllBytes(Paths.get(baseDirectory + f))));
				md.reset();
			} catch (NoSuchAlgorithmException e) {
				logger.writeTCP("sha512 not supported", LogLevel.Error);
			} catch (IOException e) {
				logger.writeTCP("cannot read " + f, LogLevel.Warning);
			}
		}
	}

	/** Send an internal error message.
	 * @param pd	ProtocolP2PPacketTCP to respond
	 */
	private void sendInternalError(ProtocolP2PPacketTCP pd) {
		logger.writeTCP("Internal Error", LogLevel.Warning);
		try {
			pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP(new Payload(RequestResponseCode.INTERNAL_ERROR)));
		} catch (Exception e) {
			logger.writeTCP(e, LogLevel.Error);
		}
	}

	/** Send response to list request
	 * @param pd	Request received
	 */
	private void sendListResponse(ProtocolP2PPacketTCP pd) {
		try {
			if (fileList.length == 0) {
				logger.writeTCP("Sending EMPTY_DIRECTORY", LogLevel.Action);
				pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP(new Payload(RequestResponseCode.EMPTY_DIRECTORY)));
			} else {
				logger.writeTCP("Sending LIST_RESPONSE", LogLevel.Action);
				pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP((Payload)(new FileList(fileList))));
			}
		} catch (Exception e2) {
			logger.writeTCP(e2, LogLevel.Error);
		}
	}

	/** Send hash response to hash request
	 * @param pd	Request received
	 */
	private void sendHashResponse(ProtocolP2PPacketTCP 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(fileList, filename) >= 0) {
				Map<HashAlgorithm, byte[]> hashes = new HashMap<>();
				for (HashAlgorithm h : ((HashRequest)p).getAlgoList()) {
					switch (h) {
						case SHA512:
							hashes.put(h, sha512.get(filename));
							break;
						case MD5:
						default:
							hashes.put(h, new byte[0]);
							break;
					}
				}
				try {
					pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP((Payload)(new HashResponse(filename, hashes))));
				} catch (Exception e) {
					logger.writeTCP(e, LogLevel.Error);
				}
			} else {
				// file not found
				try {
					pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP(new Payload(RequestResponseCode.NOT_FOUND)));
				} catch (Exception e) {
					logger.writeTCP(e, LogLevel.Error);
				}
			}
		}
	}
			

	/** Send response to load request
	 * @param pd	Request received
	 */
	private void sendLoadResponse(ProtocolP2PPacketTCP 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 {
				byte[] fullLoad = Files.readAllBytes(Paths.get(baseDirectory + filename));
				long sizeToSend = 0;
				if (fullLoad.length - offset < maxSizePartialContent) {
					logger.writeTCP("Sending last partialContent", LogLevel.Debug);
					sizeToSend = fullLoad.length - offset;
				} else {
					sizeToSend = maxSizePartialContent;
				}
				logger.writeTCP("maxSizePartialContent: " + maxSizePartialContent, LogLevel.Debug);
				logger.writeTCP("Sending " + filename + " from " + offset + " to " + (offset + sizeToSend), LogLevel.Debug);
				byte[] load = Arrays.copyOfRange(fullLoad, (int)offset, (int)(offset + sizeToSend));
				if (Arrays.binarySearch(fileList, filename) >= 0) {
					try {
						if (load.length == 0) {
							pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP(new Payload(RequestResponseCode.EMPTY_FILE)));
						} else {
							pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP((Payload)(new FilePart(filename, fullLoad.length, offset, load))));
						}
					} catch (Exception e2) {
						logger.writeTCP(e2, LogLevel.Error);
					}
				} else {
					logger.writeTCP("File requested not found: `" + filename + "` " + Arrays.binarySearch(fileList, filename), LogLevel.Debug);
					logger.writeTCP("File list:", LogLevel.Debug);
					for (String f: fileList) {
						logger.writeTCP("- " + f, LogLevel.Debug);
					}

					throw new IOException(); // to send a NOT_FOUND in the catch block
				}
			} catch (IOException e) {
				try {
					pd.sendResponse((ProtocolP2PPacket)new ProtocolP2PPacketTCP(new Payload(RequestResponseCode.NOT_FOUND)));
				} catch (Exception e2) {
					logger.writeTCP(e2, LogLevel.Debug);
				}
			}
		}
	}

}
