package ftp_client;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.concurrent.Semaphore;

// Tache de download de fichier, herite de la classe Task
public class DownloadTask extends Task {

	// Buffer Size
	// Taille du tampon pour les transferts (arbitrairement a 32Ko)
	final static int BUFFER_SIZE = 1024 *32;

	// Shared commands FTP connection
	// Recuperation d'une ancienne connexion de commande pour eviter d'en refaire une
	private static Semaphore s = new Semaphore(0); {s.release();}
	private static FTP free_ftp = null;
	
	// Current and base FTP connection
	// La connexion FTP du transfert, et celle d'origine (pour copier les parametres de connexion)
	private FTP 	ftp, oftp;

	/* Constructor */
	// On cree la table de telechargement en precisant l'ordonnanceur (la tache s'ajoute elle-meme a l'ordonnanceur),
	// le fichier source (distant), le fichier de destination (local),
	// le repertoire distant, et l'offset en cas de reprise de transfert
	public DownloadTask(Scheduler scheduler, FTP ftp, String srcFile, String dstFile, String rmtPath, long offset) throws Exception {
		super(scheduler);
		this.oftp = ftp;
		this.srcFile = srcFile;
		this.dstFile = dstFile;
		this.rmtPath = rmtPath;
		this.bytesSent = (offset>0?offset:0); // Au cas ou l'offset soit inferieur a zero du a une negligeance... Juste par principe
		this.fileSize = 0;
	}

	/* Task's Work */
	// Le travail de la tache
	protected void doWork(){		
		try{
			// On essaye de recuperer une connexion FTP de commande (donnee partagee donc semaphore)
			s.acquire();
			try{
				if(free_ftp != null){	// Si une connexion est disponible, la prendre et mettre null la ou on l'a pris
					ftp = free_ftp;
					free_ftp = null;
				}else{					// Sinon recreer une connexion a partir de la connexion d'origine
					ftp = new FTP();
					ftp.connect(oftp);
					ftp.bin(); // En mode binaire, pour les transferts
				}
			}catch(Exception e){ s.release(); throw e; }
			s.release();
			
			// Passera a TRUE lorsque qu'on aura besoin d'envoyer la commande ABOR pour annuler proprement l'action en cours si besoin est
			boolean needToAbort = false;
			
			// On se met dans le bon repertoire
			ftp.cwd(rmtPath);
			
			// On cree une nouvelle connexion de donnee
			MSocket dataSocket = ftp.PASV();

			String response="";
			
			// On recupere la taille du fichier a telecharger
		    if(fileSize==0){
		    	response = ftp.command("SIZE \""+ srcFile+"\"");
		    	if(!response.startsWith("213 ")) throw new Exception(); // On verifie si le serveur veut bien nous donner la reponse
		    	fileSize = Integer.parseInt(response.substring(response.indexOf(" ")+1)); // Parsing de la reponse (sous la forme "CODE TAILLE", on recupere le texte après l'espace et on converti en entier
		    }
		    
		    // Si on reprend un transfert, utiliser la commande REST pour preciser a partir d'ou on reprend
		    if(bytesSent > 0){	
		    	response = ftp.command("REST "+ bytesSent);
		    	if(!response.startsWith("350")) bytesSent = 0;
		    }
		    
		    // Creation du flux sur un fichier local
		    FileOutputStream output = new FileOutputStream(dstFile, bytesSent!=0);
		    
		    // Commande de telechargement
		    response = ftp.command("RETR \""+ srcFile+"\"");
		    
		    // On verifie si le serveur veut bien
		    if (!response.startsWith("125") && !response.startsWith("150") && !response.startsWith("350"))
		      throw new Exception(response);

		    // On a effectue la commande RETR, a partir de ce moment la il faut faire la commande ABOR si on a besoin d'annuler pour annuler proprement
		    needToAbort = true;

		    // Recuperation du flux d'entre du socket
		    InputStream input = dataSocket.getInputStream();
		    
		    // Processus de transfert par tampon de taille BUFFER_SIZE
		    byte[] buffer = new byte[BUFFER_SIZE];
		    int bytesRead = 0;
		    
		    // Tant qu'on nous a pas demander d'arreter, qu'on a pas atteind la taille du fichier et que la communication ne plante pas (bytesRead = -1)
		    while (!(stopped = hasToStop()) && bytesRead < fileSize && ((bytesRead = input.read(buffer)) != -1)){
		    	output.write(buffer, 0, bytesRead); // Ecrire ce qu'on a lu sur le flux du socket dans le flux du fichier
		    	bytesSent += bytesRead;				// Mettre a jour ou on en est dans le transfert
		    }
		    
		    // Si on a atteind la taille du fichier, alors on ne se considere pas comme arrete meme si on nous a demande de s'arreter
		    stopped = (bytesSent < fileSize);
		    
		    // Si on a ete arrete (erreur de communication ou demande par l'ordonnanceur) et qu'on a besoin d'envoyer la commande ABOR
		    if (stopped && needToAbort){
		    	ftp.readAllText(); // flush
		    	// Alors l'envoyer
		    	response = ftp.command("ABOR");
			    while(!response.startsWith("226") && !response.startsWith("225"))
			    	response = ftp.readLine();
		    }
		    
		    // On ferme les flux d'entre et de sortie
		    input.close();
		    output.flush(); // On flush pour bien ecrire ce qui reste dans le tampon du flux avant de le fermer (sinon on risque de perdre les dernieres donnees)
		    output.close();

		    // On ferme la connexion de donnee
		    dataSocket.close();
		    
		    // On met a disposition la connexion de commande 
		    s.acquire();
		    free_ftp = ftp;
		    s.release();
		    
		    // Si on a ete stoppe sans que nous l'ait demande, c'est qu'il y a eu une erreur de communication
		    if(stopped && !hasToStop()) throw new Exception("Problème de transfert.");
		} catch (Exception e) { // En cas d'exception
			stopped = true; // Indiquer qu'on a stoppe la tache
			aborted = (++ExceptionCount) == MaxExceptionCount; // Incremente le nombre de tentative, et si on atteind le maximum on annule completement la tache
			if(!aborted){ // Si il reste des tentatives, l'indiquer, attendre un certain temps et retenter le transfert
				int remaining = (MaxExceptionCount-ExceptionCount);
				String pluriel = remaining>1?"s":""; 
				Message.sendError("Erreur lors du download de \""+srcFile+"\".\n"+e.getMessage()+"\nProchaine tentative dans 3 secondes. ("+remaining+" tentative"+pluriel+" restante"+pluriel+")");
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e1) {}
			}else{ // Sinon afficher une erreur
				Message.sendError("Impossible de downloader \""+srcFile+"\".");
			}
			// Si on a annule, on l'indique aux autres
			if(aborted) fireAbortedListener(this);
		}
	}

}
