package clientP2P;

import java.util.Arrays;
import java.util.Scanner;
import java.util.List;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import localException.ProtocolError;
import localException.InternalError;
import localException.ProtocolError;
import localException.SizeError;
import localException.TransmissionError;
import localException.VersionError;
import localException.SocketClosed;
import protocolP2P.RequestResponseCode;
import protocolP2P.FileList;
import protocolP2P.ProtocolP2PPacket;
import protocolP2P.DiscoverRequest;
import protocolP2P.DiscoverResponse;
import protocolP2P.Payload;
import protocolP2P.HashAlgorithm;
import remoteException.EmptyFile;
import remoteException.EmptyDirectory;
import remoteException.InternalRemoteError;
import remoteException.NotFound;
import remoteException.ProtocolRemoteError;
import remoteException.VersionRemoteError;
import remoteException.NotATracker;
import remoteException.UnknownHost;
import tools.ServeErrors;
import tools.HostItem;
import tools.Logger;
import tools.LogLevel;
import java.net.SocketException;

/** Implementation of P2P-JAVA-PROJECT CLIENT
 * @author	Louis Royer
 * @author	Flavien Haas
 * @author	JS Auge
 * @version	1.0
 */
public abstract class ClientManagement extends ServeErrors implements Runnable {
	protected String baseDirectory;
	protected String partsSubdir;
	protected List<HostItem> hostList;
	protected HostItem tracker;
	protected HostItem client;
	protected Logger logger;
	protected Scanner scanner;
	protected ClientDownload downLoader;

	/** Constructor with baseDirectory, tracker, partsSubdir, logger, and scanner parameters.
	 * @param baseDirectory	the root directory where files are stored
	 * @param tracker Tracker hostItem
	 * @param partsSubdir	subdirectory to store file parts
	 * @param logger	Loggger
	 * @param scanner	Scanner used to read input
	 * @param client	HostItem of the application
	 */
	public ClientManagement(String baseDirectory, HostItem tracker, String partsSubdir, Logger logger, Scanner scanner, HostItem client) {
		this.scanner = scanner;
		this.baseDirectory = baseDirectory;
		this.tracker = tracker;
		this.partsSubdir = partsSubdir;
		this.logger = logger;
		this.client = client;
		try {
			initHostList();
		} catch (InternalError e) {
			System.exit(-1);
		} catch (ProtocolError e) {
			System.exit(-2);
		}
	}

	/** Getter for tracker socket
	 * @return	Tracker's socket
	 * @throws	SocketException
	 * @throws	UnknownHostException
	 * @throws	IOException
	 */
	protected abstract Object getTrackerSocket() throws SocketException, UnknownHostException, IOException;

	/** Close Tracker socket
	 */
	protected abstract void closeTrackerSocket();


	/** Initialize hostList from tracker
	 * @throws	ProtocolError
	 * @throws	InternalError
	 */
	private void initHostList() throws ProtocolError, InternalError {
		ProtocolP2PPacket<?> d = createProtocolP2PPacket(new DiscoverRequest(null));
		try {
			boolean contacted = false;
			boolean firstLoop = true;
			boolean stop = false;
			while (!contacted && !stop) {
				try {
					if (!firstLoop) {
						writeLog("Cannot contact tracker. Try again [Y/n] ?", LogLevel.Error);
						String tryAgain = scanner.nextLine();
						if (tryAgain.equals("n") || tryAgain.equals("N")) {
							stop = true;
						}
					}
					firstLoop = false;
					d.sendRequest(getTrackerSocket());
					contacted = true;
				} catch (SocketException e) {
				} catch (UnknownHostException e) {
				} catch (IOException e) {
				}
			}
			if (stop) {
				System.exit(3);
			}
			Payload p = d.receiveResponse().getPayload();
			assert p instanceof DiscoverResponse : "This payload must be instance of Filelist";
			if (!(p instanceof DiscoverResponse)) {
				throw new InternalError();
			} else {
				hostList = ((DiscoverResponse)p).getHostList();
			}
			closeTrackerSocket();
		} catch (SocketClosed e){
			writeLog("listDirectory : SocketClosed", LogLevel.Error);
			throw new ProtocolError();
		} catch (NotATracker e) {
			writeLog(e, LogLevel.Error);
			throw new ProtocolError();
		} catch (Exception e) {
			writeLog(e, LogLevel.Error);
			throw new ProtocolError();
		}
	}

	/** Compute Hashsum of a file.
	 * @param	filename
	 * @return	hashsum
	 */
	protected byte[] computeHashsum(String filename, HashAlgorithm h) {
		try {
			MessageDigest md = MessageDigest.getInstance(HashAlgorithm.SHA512.getName());
			return md.digest(Files.readAllBytes(Paths.get(baseDirectory + filename)));
		} catch (NoSuchAlgorithmException e) {
			writeLog(h.getName() + " not supported", LogLevel.Error);
		} catch (IOException e) {
			writeLog("cannot read " + filename, LogLevel.Error);
		}
		return new byte[0];
	}

	/** 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);


	/** list server’s directory content
	 * @return	list of files
	 * @throws	InternalError
	 * @throws	UnknowHostException
	 * @throws	IOException
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	SizeError
	 * @throws	EmptyDirectory
	 * @throws	InternalRemoteError
	 * @throws	ProtocolRemoteError
	 * @throws	VersionRemoteError
	 */
	protected String[] listDirectory() throws EmptyDirectory, InternalError, UnknownHostException, IOException, TransmissionError, ProtocolError, VersionError, SizeError, InternalRemoteError, ProtocolRemoteError, VersionRemoteError {
		if (hostList.size() == 0) {
			return new String[0];
		}
		ProtocolP2PPacket<?> d = createProtocolP2PPacket(new Payload(RequestResponseCode.LIST_REQUEST));
		try {
			d.sendRequest(getHostItemSocket(hostList.get(0)));
			Payload p = d.receiveResponse().getPayload();
			closeHostItemSocket(hostList.get(0));
			assert p instanceof FileList : "This payload must be instance of Filelist";
			if (!(p instanceof FileList)) {
				throw new InternalError();
			} else {
				return ((FileList)p).getFileList();
			}
		} catch (NotFound e) {
			writeLog(e, LogLevel.Error);
			throw new ProtocolError();
		} catch (EmptyFile e) {
			writeLog(e, LogLevel.Error);
			throw new ProtocolError();
		} catch (SocketClosed e){
			writeLog("listDirectory : SocketClosed", LogLevel.Error);
			throw new ProtocolError();
		} catch (NotATracker e) {
			writeLog(e, LogLevel.Error);
			throw new ProtocolError();
		} catch (UnknownHost e) {
			writeLog(e, LogLevel.Error);
			throw new ProtocolError();
		}
	}

	/** Initialize downloader
	 * @param filename	Name of the file to download
	 */
	protected abstract void initDownloader(String filename);

	/** Try to download a file
	 * @param filename	name of the file to download
	 * @throws	NotFound
	 * @throws	InternalError
	 * @throws	UnknownHostException
	 * @throws	IOException
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	SizeError
	 * @throws	InternalRemoteError
	 * @throws	ProtocolRemoteError
	 * @throws	VersionRemoteError
	 * @throws	EmptyFile
	 */
	private void download(String filename) throws EmptyFile, NotFound, InternalError, UnknownHostException, IOException, TransmissionError, ProtocolError, VersionError, SizeError, InternalRemoteError, ProtocolRemoteError, VersionRemoteError {
		initDownloader(filename);
		Thread t = new Thread(downLoader);
		t.start();
		try {
			t.join();
			if (downLoader.getSuccess()) {
				byte[] hash512 = downLoader.getHashSum512();
				if (!Arrays.equals(hash512, computeHashsum(filename, HashAlgorithm.SHA512))) {
					writeLog("Hashsum does not match", LogLevel.Error);
					String line = "Computed checksum:\n";
					byte[] c = computeHashsum(filename, HashAlgorithm.SHA512);
					for (byte b: c) {
						line += String.format("%02X", b);
					}
					line += "\nReceived checksum:\n";
					for (byte b: hash512) {
						line += String.format("%02X", b);
					}
					line += "\n";
					writeLog(line, LogLevel.Info);
					throw new InternalError();
				} else {
					downLoader.sendRatioUpdate();
					writeLog("Ratio updates sent.", LogLevel.Info);
				}
			} else {
				throw new InternalError();
			}
		} catch (InterruptedException e) {
			throw new InternalError();
		}
	}

	/** Implementation of Runnable
	 */
	public void run() {
		boolean isrunning = true;
		while (isrunning){
			try {
				int i = 1;
				String[] list = listDirectory();
				System.out.println("Files present on the server:");
				System.out.println("0 : Exit the program");
				for(String listItem: list) {
					System.out.println(i + " : " + listItem);
					i++;
				}
				System.out.println("Type the number associated with the file to download:");
				String f = scanner.nextLine();
				if(f.equals("0")){
					isrunning = false;
				}
				else{
					int j = Integer.parseInt(f);
					if(j <= list.length){
						j--;
						download(list[j]);
						System.out.println("File " + f + " sucessfully downloaded");
						writeLog("File " + f + " sucessfully downloaded", LogLevel.Info);
					}
					else{
						System.out.println("File " + f + " unsucessfully downloaded, wrong number");
						writeLog("File " + f + " unsucessfully downloaded, wrong number", LogLevel.Info);
					}
				}
			} catch (EmptyDirectory e) {
				writeLog("Server has no file in directory", LogLevel.Error);
			} catch (InternalError e) {
				writeLog("Client internal error", LogLevel.Error);
			} catch (UnknownHostException e) {
				writeLog("Server host is unknown", LogLevel.Error);
			} catch (IOException e) {
				writeLog("Request cannot be send or response cannot be received", LogLevel.Error);
			} catch (TransmissionError e) {
				writeLog("Message received is too big", LogLevel.Error);
			} catch (ProtocolError e) {
				writeLog("Cannot decode server’s response", LogLevel.Error);
			} catch (VersionError e) {
				writeLog("Server’s response use bad version of the protocol", LogLevel.Error);
			} catch (SizeError e) {
				writeLog("Cannot handle this packets because of internal representation limitations of numbers on the client", LogLevel.Error);
			} catch (InternalRemoteError e) {
				writeLog("Server internal error", LogLevel.Error);
			} catch (ProtocolRemoteError e) {
				writeLog("Server cannot decode client’s request", LogLevel.Error);
			} catch (VersionRemoteError e) {
				writeLog("Server cannot decode this version of the protocol", LogLevel.Error);
			} catch (NotFound e) {
				writeLog("Server has not this file in directory", LogLevel.Error);
			} catch (EmptyFile e) {
				writeLog("File is empty", LogLevel.Error);
			}
		}
		writeLog("Exiting client", LogLevel.Info);
	}
}
