package clientP2P;
import exception.InternalError;
import exception.ProtocolError;
import exception.SizeError;
import exception.TransmissionError;
import exception.VersionError;
import exception.SocketClosed;
import remoteException.EmptyFile;
import remoteException.EmptyDirectory;
import remoteException.InternalRemoteError;
import remoteException.NotFound;
import remoteException.ProtocolRemoteError;
import remoteException.VersionRemoteError;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.Socket;
import java.io.IOException;
import java.nio.file.Files;
import java.io.File;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import protocolP2P.ProtocolP2PPacketTCP;
import protocolP2P.Payload;
import protocolP2P.RequestResponseCode;
import protocolP2P.FileList;
import protocolP2P.FilePart;
import protocolP2P.LoadRequest;
import protocolP2P.HashAlgorithm;
import protocolP2P.HashRequest;
import protocolP2P.HashResponse;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/** Implementation of P2P-JAVA-PROJECT CLIENT
 * @author	Louis Royer
 * @author	Flavien Haas
 * @author	JS Auge
 * @version	1.0
 */
public class ClientManagementTCP implements Runnable {
	private String baseDirectory;
	private int TCPPort;
	private String host;
	private Socket socket;



	/** Constructor for TCP implementation, with baseDirectory and TCPPort parameters.
	 * @param baseDirectory	the root directory where files are stored
	 * @param host	hostname of the server
	 * @param TCPPort	the server will listen on this port
	 */
	public ClientManagementTCP(String baseDirectory, String host, int TCPPort) {
		this.baseDirectory = baseDirectory;
		this.host = host;
		this.TCPPort = TCPPort;
		try {
			socket = new Socket(InetAddress.getByName(host), TCPPort);
		} catch (SocketException e) {
			System.err.println("Error: No TCP socket available.");
			System.exit(-1);
		} catch (UnknownHostException e) {
			System.err.println("Error: Unknown host.");
			System.exit(-1);
		} catch (IOException e) {
			System.err.println("Error: Cannot create TCP socket");
			System.exit(-1);
		}
	}

	/** Implementation of Runnable
	 */
	public void run() {
		try {
			String[] list = listDirectory();
			System.out.println("Files present on the server:");
			for(String listItem: list) {
				System.out.println(listItem);
			}
			System.out.println("Name of the file to download:");
			Scanner scanner = new Scanner(System.in);
			String f = scanner.nextLine();
			download(f);
			System.out.println("File sucessfully downloaded");
		} catch (EmptyDirectory e) {
			System.err.println("Error: Server has no file in directory");
		} catch (InternalError e) {
			System.err.println("Error: Client internal error");
		} catch (UnknownHostException e) {
			System.err.println("Error: Server host is unknown");
		} catch (SocketClosed e) {
			System.err.println("Error: Request cannot be send or response cannot be received");
		} catch (IOException e) {
			System.err.println("Error: Request cannot be send or response cannot be received");
		} catch (TransmissionError e) {
			System.err.println("Error: Message received is too big");
		} catch (ProtocolError e) {
			System.err.println("Error: Cannot decode server’s response");
		} catch (VersionError e) {
			System.err.println("Error: Server’s response use bad version of the protocol");
		} catch (SizeError e) {
			System.err.println("Error: Cannot handle this packets because of internal representation limitations of numbers on the client");
		} catch (InternalRemoteError e) {
			System.err.println("Error: Server internal error");
		} catch (ProtocolRemoteError e) {
			System.err.println("Error: Server cannot decode client’s request");
		} catch (VersionRemoteError e) {
			System.err.println("Error: Server cannot decode this version of the protocol");
		} catch (NotFound e) {
			System.err.println("Error: Server has not this file in directory");
		} catch (EmptyFile e) {
			System.err.println("Error: File is empty");
		} finally {
			try {
				System.err.println("Closing socket");
				socket.close();
			} catch (IOException e2) {
				System.err.println("Error: cannot close socket");
			}
		}
	}

	/** Try to download a file
	 * @param filename	name of the file to download
	 * @throws	NotFound
	 * @throws	InternalError
	 * @throws	UnknownHostException
	 * @throws	IOException
	 * @throws	SocketClosed
	 * @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, SocketClosed, TransmissionError, ProtocolError, VersionError, SizeError, InternalRemoteError, ProtocolRemoteError, VersionRemoteError {
		byte [] hash512;
		final long MAX_PARTIAL_SIZE = 4096;
		HashAlgorithm[] hashesAlgo = new HashAlgorithm[1];
		hashesAlgo[0] = HashAlgorithm.SHA512;
		ProtocolP2PPacketTCP d = new ProtocolP2PPacketTCP((Payload) new HashRequest(filename, hashesAlgo));
		d.sendRequest((Object)socket);
		try {
			Payload pHash = d.receiveResponse().getPayload();
			assert pHash instanceof HashResponse : "This payload must be instance of HashResponse";
			if (!(pHash instanceof HashResponse)) {
				throw new InternalError();
			} else { 
				hash512 = ((HashResponse)pHash).getHash(HashAlgorithm.SHA512);
				if (hash512.length == 0) {
					System.err.println("Error: server do not support sha512 hashes");
					throw new InternalError();
				}
			}
		} catch (EmptyDirectory e) {
			System.err.println("Error: empty directory, but request was not LIST_REQUEST");
			throw new ProtocolError();
		}

		d = new ProtocolP2PPacketTCP((Payload) new LoadRequest(filename, 0, MAX_PARTIAL_SIZE));
		d.sendRequest((Object)socket);
		boolean fileFullyWritten = false;
		long offset = 0;
		do {
			try {
				Payload p = d.receiveResponse().getPayload();
				assert p instanceof FilePart : "This payload must be instance of FilePart";
				if (!(p instanceof FilePart)) {
					throw new InternalError();
				} else {
					FilePart fp = (FilePart)p;
					if (!fp.getFilename().equals(filename)) {
						System.err.println("Error: wrong file received: `" + fp.getFilename() + "`");
						throw new ProtocolError();
					}
					if (fp.getOffset() == 0) {
						System.err.println("Receiving first partialContent");
						// first partialContent
						// increment offset
						offset = fp.getPartialContent().length;
						/* write first partialContent */
						try {
							Files.write(new File(baseDirectory + filename).toPath(), fp.getPartialContent());
						} catch (IOException e) {
							System.err.println("Error: cannot write file (" + baseDirectory + filename + ")");
						}
						// next partialContentRequest
						if (offset != fp.getTotalSize()) {
							System.err.println("Sending following request with offset: " + offset + " maxpartialsize: " + MAX_PARTIAL_SIZE);
							d = new ProtocolP2PPacketTCP((Payload) new LoadRequest(filename, offset, MAX_PARTIAL_SIZE));
							d.sendRequest((Object)socket);
						} else {
							fileFullyWritten = true;
						}
					} else if (offset == fp.getOffset()){
						System.err.println("Receiving following partialContent (offset: " + offset + ")");
						// following
						// increment offset
						offset += fp.getPartialContent().length;
						/* write following partialContent at end of file*/
						try {
							Files.write(Paths.get(baseDirectory + filename), fp.getPartialContent(), StandardOpenOption.APPEND);
						} catch (IOException e) {
							System.err.println("Error: cannot write file (" + baseDirectory + filename + ")");
						}
						if (offset >= fp.getTotalSize()) {
							fileFullyWritten = true;
						} else {
							// next partialContentRequest
							d = new ProtocolP2PPacketTCP((Payload) new LoadRequest(filename, offset, MAX_PARTIAL_SIZE));
							d.sendRequest((Object)socket);
						}

					} else {
						System.err.println("offset: " + fp.getOffset() + " ; content.length: " + fp.getPartialContent().length + " ; totalSize: " + fp.getTotalSize());
						System.err.println("Error: cannot handle non-consecutive partial files (not implemented)");
						throw new InternalError();
					}
				}
			} catch (EmptyDirectory e) {
				throw new ProtocolError();
			}
		} while(!fileFullyWritten);
		if (!Arrays.equals(hash512, computeHashsum(filename, HashAlgorithm.SHA512))) {
			System.err.println("Error: Hashsum does not match");
			System.err.println("Computed checksum:");
			byte[] c = computeHashsum(filename, HashAlgorithm.SHA512);
			for (byte b: c) {
				System.err.print(String.format("%02X", b));
			}
			System.err.println("");
			System.err.println("Received checksum:");
			for (byte b: hash512) {
				System.err.print(String.format("%02X", b));
			}
			System.err.println("");
			throw new InternalError();
		}
		socket.close();
	}

	/** list server’s directory content
	 * @return	list of files
	 * @throws	InternalError
	 * @throws	UnknowHostException
	 * @throws	SocketClosed
	 * @throws	IOException
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	SizeError
	 * @throws	EmptyDirectory
	 * @throws	InternalRemoteError
	 * @throws	ProtocolRemoteError
	 * @throws	VersionRemoteError
	 */
	private String[] listDirectory() throws EmptyDirectory, InternalError, UnknownHostException, SocketClosed, IOException, TransmissionError, ProtocolError, VersionError, SizeError, InternalRemoteError, ProtocolRemoteError, VersionRemoteError {
		ProtocolP2PPacketTCP d = new ProtocolP2PPacketTCP(new Payload(RequestResponseCode.LIST_REQUEST));
		d.sendRequest((Object)socket);
		try {
			Payload p = d.receiveResponse().getPayload();
			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) {
			throw new ProtocolError();
		} catch (EmptyFile e) {
			throw new ProtocolError();
		}
	}

	/** Compute Hashsum of a file.
	 * @param	filename
	 * @return	hashsum
	 */
	private 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) {
			System.out.println("Error: " + h.getName() + " not supported");
		} catch (IOException e) {
			System.out.println("Error: cannot read " + filename);
		}
		return new byte[0];
	}
}
