package ftp_client;
import java.util.ArrayList;
import java.util.StringTokenizer;

// classe de communication FTP
// aucune commande FTP n'est présente dans les autres fichiers,
// excepté dans UploadTask.java et DownloadTask.java
// qui gèrent eux-même les transferts
public class FTP {
	
	/* Variables */
	// Nom d'utilisateur, mot de passe, serveur, dossier courant
	public String user, password, host, currentPath;
	// Port de connexion
	public int port;

	/* Socket */
	private MSocket socket = null;
	public MSocket getSocket(){ return socket; }
	
	/* Connect */
	// Connexion via un autre objet FTP
	public synchronized void connect(FTP _ftp) throws Exception {
		connect(_ftp.host, _ftp.port, _ftp.user, _ftp.password);
	}
	
	// Connexion via les informations de FTPInfo
	public synchronized void connect(FTPInfo fi) throws Exception {
		connect(fi.address, fi.port, fi.user, fi.password);
	} 
	
	// Connexion par arguments "classiques"
	//USER <nom> : 331 ..., PASS <mdp> : 230 ...
	public synchronized void connect(String host, int port, String user, String pass) throws Exception {
		try{
			// Creation d'un socket simplifie (voir MSocket.java)
			socket = new MSocket(host, port);
		
			// On lit le message de bienvenue
			String response = socket.readLine();
			if (!response.startsWith("220"))	throw new Exception(response);
			while(response.startsWith("220-"))	response = socket.readLine(); // Des fois les messages sont composes de plusieurs lignes, commencant par "220-", la derniere ligne commence par "220 "

			// On envoi les commandes d'identification, si le code de retour n'est pas bon, renvoyer une erreur
			if (!command("USER " + user).startsWith("331"))	throw new Exception();
			if (!command("PASS " + pass).startsWith("230"))	throw new Exception();
		}catch(Exception e){
			// Si erreur il y a, supprimer le socket et renvoyer encore l'erreur
			socket = null;
			throw e;
		}
		
		// Si on arrive la c'est que tout est OK, alors on enregistre les parametres
		this.host = host;
		this.port = port;
		this.user = user;
		this.password = pass;
		// Le repertoire courant de base est la racine
		currentPath = "/";
	}

	/* Disconnect */
	// Procedure de deconnexion
	// QUIT
	public synchronized void disconnect() {
		if(socket==null) return; // Si on est deja deconnecte, quitter
		try {command("QUIT");} catch (Exception e) {} // Envoyer la commande pour quitter
		socket = null; // Supprimer le socket
	}
	
	/* Reconnect */
	// Tentative de reconnexion avec les parametres enregistres
	public boolean reconnect(){
		try{
			connect(host, port, user, password);
			return true;
		}catch(Exception e){}
		return false;
	}
	  
	/* Make dir */
	// Creation d'un nouveau dossier, dans le meme repertoire que le parametre "file" si il s'agit d'un fichier, ou dedans si il s'agit d'un dossier
	// MKD <nom_du_dossier> : 257 ...
	public synchronized boolean makeDir(FTPFile file, String name){
	  try {
		  String base = "";
		  if(file != null){ // On recupere le chemin de file
			  if(file.isDirectory()) base = file.getAbsolutePath();
			  else					 base = file.getParentPath();
		  }
		  if(!base.endsWith("/")) base += "/";
		  // On envoi la commande pour creer le dossier, si le code de retour est bon, renvoyer vrai
		  if(command("MKD \""+base+name+"\"").startsWith("257")) return true;
	  } catch(Exception e){}
	  // Autrement renvoyer faux
	  return false;
	}
  
	/* Delete file */
	// Effacer un fichier ou dossier
	// RMD <dossier>, DELE <fichier> : 250 ...
	public synchronized boolean delete(FTPFile file){
	  try {
		  // Si il s'agit d'un dossier, faire des appels recursifs pour effacer le contenu, puis effacer le dossier
		  if(file.isDirectory()){
			  ArrayList<FTPFile> files = getFiles(file.toString());
			  for(FTPFile f : files) delete(f);
			  // Envoyer la commande pour effacer un dossier, renvoyer faux si le code de retour est mauvais
			  if(!command("RMD \""+file+"\"").startsWith("250")) return false;
		  // Envoyer la commande pour effacer un fichier, renvoyer faux si le code de retour est mauvais
		  }else if(!command("DELE \""+file+"\"").startsWith("250")) return false;
		  // Renvoyer vrai
		  return true;
	  } catch(Exception e){}
	  // Si une erreur est survenue, renvoyer faux
	  return false;
	}
	
	/* Rename file */
	// Renommer un fichier
	// RNFR <fichier_reference> : 350 ..., RNTO <fichier_destination> : 250 ...
	public boolean rename(FTPFile file, String newname){
		try {
			// On prend le fichier de reference, si le code retour est mauvaix, renvoyer faux
			if(!command("RNFR \""+file.getAbsolutePath()+"\"").startsWith("350"))		  return false;
			// Et on indique le fichier "destination", si le code retour est mauvaix, renvoyer faux
			if(!command("RNTO \""+file.getParentPath()+newname+"\"")  .startsWith("250")) return false;
			// Puis on change le nom dans l'objet file
			file.setName(newname);
			// Renvoyer vrai
			return true;
		} catch (Exception e) {}
		// Renvoyer faux
		return false;
	}
   
	/* File exists */
	// Verifier si un fichier existe (en demandant sa taille)
	// SIZE <fichier> : 213 ...
	public boolean fileExists(String path){
		try {
			return command("SIZE \""+ path +"\"").startsWith("213 ");
		} catch (Exception e) {}
		return false;
	}
	
	/* File size */
	// Recuperer la taille d'un fichier
	// SIZE <fichier> : 213 <taille>
	public long fileSize(String path){
		try {
			String str = command("SIZE \""+ path +"\"");
			if(!str.startsWith("213 ")) return -1; // Si le code retour n'est pas bon, renvoyer -1
			return Long.parseLong(str.substring(str.indexOf(" ")+1)); // Sinon convertir la taille en long et la renvoyer
		} catch (Exception e) {}
		return -1; // En cas de probleme, renvoyer -1
	}
	
	/* Get current dir */
	// Recuperer le repertoire courant
	// PWD : 257 "<repertoire>"
	public synchronized String getCurrentDirectory() throws Exception {
		String response = command("PWD");
		if(!response.startsWith("257")) return null;
		int quote1 = response.indexOf('\"'); // On recupere la position du premier guillement...
		int quote2 = response.indexOf('\"', quote1 + 1); // ...et du prochain
		if(quote2 > 0) return response.substring(quote1 + 1, quote2); // Renvoyer la chaine de retour decoupee
		return null; // En cas de probleme (pas 2 guillemets), renvoyer null
	}

	/* Change the current dir */
	// Changer le repertoire courant
	// CWD <dossier_cible> : 250 ...
	public synchronized boolean cwd(String dir) {
		// On remplace tous les // en / tant qu'il y en reste, et on fini le chemin par un /
		while(dir.contains("//")) dir = dir.replace("//", "/");
		if(!dir.endsWith("/")) dir += "/";

		if(dir.equals(currentPath)) return true;

		try{
			if(command("CWD " + dir).startsWith("250")){
				currentPath = dir; // Le changement a bien ete effectue, alors on met a jour currentPath
				return true;
			}
		}catch(Exception e){}
		
		return false;
	}
  
	/* Create data connection */
	// Creation d'une connexion de donnees
	// PASV : 227 Entering Passive Mode (<ip1>,<ip2>,<ip3>,<ip4>,<port1>,<port2>)
	// Connexion a ip1.ip2.ip3.ip4 sur son port (port1 * 256 + port2)
	public synchronized MSocket PASV() throws Exception {
		String response = command("PASV");
		while (!response.startsWith("227"))
			response = readLine();

		String ip = null;
		int port = -1;
		// Parsing de la reponse, on recupere ce qu'il y a entre les parentheses
		int paren1 = response.indexOf('(');
		int paren2 = response.indexOf(')', paren1 + 1);
		if(paren2 > 0){ // Si c'est faisable (les 2 parentheres sont bien presentes)
		  String tmp = response.substring(paren1 + 1, paren2);	// Decouper la chaine de caracetere
		  StringTokenizer tk = new StringTokenizer(tmp, ",");	// Maintenant on recupere chaque jeton entre les virgules
		  try {
			  // Puis on reconstitue l'IP
			  ip = tk.nextToken() + "." + tk.nextToken() + "." + tk.nextToken() + "." + tk.nextToken();
			  // Et le port
			  port = Integer.parseInt(tk.nextToken()) * 256 + Integer.parseInt(tk.nextToken());
			  // Et en renvoi le socket de donnees
			  return new MSocket(ip, port);
		  } catch (Exception e) {
			  // Sinon on renvoi une exception
			  throw new Exception(response);
		  }
		}
		// Autrement renvoyer null
		return null;
	}

	/* List files */
	// Lister les fichiers du repertoire courant
	public synchronized ArrayList<FTPFile> getFiles() throws Exception {
		return getFiles(null);
	}
	
	// Lister les fichiers d'un repertoire
	// PASV
	// list <dossier> : 150 ...
	public synchronized ArrayList<FTPFile> getFiles(String path) throws Exception {
		// On cree une connexion de donnees
		MSocket dataSocket = PASV();

		// On envoi la commande de listage des fichiers
		String str;
		if( !(str=command("LIST"+(path!=null?" \""+path+"\"":""))).startsWith("150") ) throw new Exception(str);

		// On cree une liste de fichier en lisant toutes les lignes disponibles dans le socket de donnees
		// (FTPFile parse lui meme la ligne)
		ArrayList<FTPFile> files = new ArrayList<FTPFile>();
		while((str = dataSocket.readLine()) != null)
			try { files.add(new FTPFile(this, str, (path==null?currentPath:path))); } catch (Exception e) {}
		readLine();
		return files;
	}
  
	/* Switch Binary/ASCII mode */
	// Passage en mode binaire (pour les fichiers binaires)
	// TYPE I : 200 ...
	public synchronized boolean bin() throws Exception {
		String response = command("TYPE I");
		return response.startsWith("200");
	}
	
	// Passage en mode ASCII (pour les fichiers textes) (jamais utilise dans le programme, tous les transferts se font de maniere binaire)
	// TYPE A : 200 ...
	public synchronized boolean ascii() throws Exception {
		String response = command("TYPE A");
		return response.startsWith("200");
	}

	/* Read/Write socket */
	// Lire une ligne sur le socket
	public synchronized String readLine() throws Exception {
		return socket.readLine();
	}
	
	// Lire tout le texte disponible sur le socket
	public synchronized String readAllText() throws Exception {
		return socket.readAllText();
	}
  
	// Envoyer une commande sur le socket, puis retourner la reponse
	public synchronized String command(String line) throws Exception {
		for(int i=0; i<3; i++){ // On effectue 3 tentatives
		  try{
			  socket.readAllText(); // On vide le texte disponible (utilise comme une sorte de flush())
			  socket.sendLine(line);
			  String rtn = socket.readLine();
			  if(rtn==null) return "";
			  return rtn; // Retourner la reponse du serveur
		  }catch(Exception e){
			  // Si il y a eu un probleme de communication, on attend 2 secondes puis on se reconnecte avant de retenter la commande
			  try {
				Thread.sleep(2000);
			  } catch (InterruptedException e1) {}
			  reconnect();
		  }
		}
		// Si au bout des 2 tentatives ça n'a toujours pas fonctionne, lancer une exception
		throw new Exception();
	}
}
