package ftp_client;
import java.io.File;
import java.util.ArrayList;
import java.util.concurrent.Semaphore;

import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Tree;

// Coeur du programme

public class MainFTP {
	
	/* Variables */
	Scheduler sch_dl, sch_ul; 	// Ordonnanceur de download, et d'upload
	FTP ftp;					// Objet FTP
	MainWindow w;				// Fenetre principale
	ConnectionForm cf;			// Fenetre de gestion des connexions
	
	/* FTP accounts */
	ArrayList<FTPInfo> comptes; // Liste des connexions enregistrees
	
	/* Constructor */
	// Programme principale
	public MainFTP(){
		
		ftp = null;
		
		// On cree la fenetre
		w = new MainWindow();
		// On relache le semaphore (utilise dans la fonction printMessage) (un semaphore est acquis de base, obligation de le relacher lors de l'initialisation)
		s.release();
		
		// La fenetre principale et le coeur sont independant, la fenetre communique avec le coeur via des ecouteurs.
		// La fenetre appelle getFiles("/") par exemple via sa fonction fireGetFiles("/"), le coeur reçoit l'appelle grâce à l'écouteur,
		// le coeur gère lui même l'appel à la fonction correspondante dans son objet FTP et renvoit le resultat à la fenetre.
		// La fenetre et les communications FTP sont notamment TOTALEMENT independante.
		w.addFTPCommandsListener(new MainWindow.FTPCommandsListener() {
			
			public ArrayList<FTPFile> getFiles(String dir) {
				if(ftp==null) return new ArrayList<FTPFile>();
				try {
					Message.send("Récupération de la liste des fichiers du dossier \""+(dir==null?"/":dir)+"\"...");
					ArrayList<FTPFile> rtn = ftp.getFiles(dir);
					Message.send("Liste des fichiers du dossier \""+(dir==null?"/":dir)+"\" récupérée.");
					return rtn;
				} catch (Exception e) {
					Message.sendError("Impossible de récupérer la liste des fichiers du dossier \""+(dir==null?"/":dir)+"\".");
					return new ArrayList<FTPFile>();
				}
			}
			
			public boolean changeDir(String dir) {
				if(ftp==null) return false;
				Message.send("Changement de répertoire courant en \""+(dir==null?"/":dir)+"\"...");
				if(ftp.cwd(dir)){
					Message.send("Changement de répertoire courant en \""+(dir==null?"/":dir)+"\" réussi.");
					return true;
				}
				Message.sendError("Impossible de changer de répertoire courant en \""+(dir==null?"/":dir)+"\".");
				return false;
			}

			public boolean makeDir(String path) {
				return MainFTP.this.makeFTPDir(null, path);
			}

			public void connect(final FTPInfo fi) {
				new Thread(){public void run(){
					if(connectFTP(fi))
						w.Connected();
				}}.start();
			}

			public void disconnect() {
				new Thread(){public void run(){
					fullFTPDisconnect();
				}}.start();
			}

			public void showConnectionManager() {
				showFTPConnectionManager();
			}
		});

		// On separe l'interface des communications FTP. De maniere a pas bloquer l'interface lors d'une requete.
		new Thread(){
			public void run(){
				// Au cas ou une exception non gérée est envoyée, on la récupère et on l'affiche dans la renvoi via la classe Message  
				Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
					public void uncaughtException(Thread arg0, Throwable arg1) {
						Message.sendError(arg1.getMessage());
					}
				});
				
				// Initialisation des ordonnanceurs, avec demarrage automatique des taches.
				sch_dl = new Scheduler();
				sch_dl.autoStart = true;
				
				sch_ul = new Scheduler();
				sch_ul.autoStart = true;
				
				// Initialisation de l'objet de communications FTP
				ftp = new FTP();

				// On lie la fonction printMessage avec les messages envoyés avec la classe Message
				Message.addMessageListener(new Message.MessageListener() {
					public void received(String message) {
						printMessage(message, MESSAGETYPE.INFO);
					}

					public void receivedError(String message) {
						printMessage(message, MESSAGETYPE.ERROR);
					}
				});
				
				// On lie la fonction printMessage avec les communications des sockets (affichage des lignes envoyees et recues lors des communications)
				MSocket.addRecvListener(new MSocket.CommunicationListener() {
					public void communicationEvent(String message) {
						printMessage(message, MESSAGETYPE.DN_MSG);
					}
				});
				MSocket.addSendListener(new MSocket.CommunicationListener() {
					public void communicationEvent(String message) {
						printMessage(message, MESSAGETYPE.UP_MSG);
					}
				});
				
				// On code chaque fonction des elements des arborescences qui necessite l'utilisation d'autres objets (transferts, changer de repertoire courant, creer un nouveau dossier, rafraichir)  
				FileTreeItem.addItemsAddedListener(new FileTreeItem.AddListener() {
					public void itemAdded(FileTreeItem sender) {
						sender.addTransfertListener(new FileTreeItem.FileTreeListener() {
							public void refreshSubItems(FileTreeItem sender) {
								if(sender.file instanceof FTPFile)
									w.changeRemoteDirectory(sender, sender.getExpanded());
								else if(sender.file instanceof File)
									w.changeLocalDirectory(sender, sender.getExpanded());
							}
							
							public void addTansfert(File file) {
								if(file instanceof FTPFile)   addDownload((FTPFile)file);
								else if(file instanceof File) addUpload(file);
							}

							public boolean newFolder(FileTreeItem sender, String name) {
								if(ftp==null) return false;
								if(name != null){
									if(sender.file instanceof FTPFile)
										return makeFTPDir((FTPFile) sender.file, name);
									else if(sender.file instanceof File)
										return Misc.makeDir(sender.file, name);
								}
								return false;
							}

							public void refreshTree(final Tree tree) {
								Display.getDefault().asyncExec(new Runnable() {
						            public void run() {
						            	if(tree == w.getLocalTree()) w.changeLocalDirectory(null, true);
						            	else						 w.changeRemoteDirectory(null, true);
									}
								});
							}

						});						
					}
				});

			}
		}.start();

		// On recupere les comptes FTP enregistres, si ils n'y en a aucun, creer une liste de compte vide
		if((comptes = ConnectionManager.readFTPInfos()) == null)
			comptes = new ArrayList<FTPInfo>();
		// Rafraichir le menu associe ("Connexion" -> "Connexion rapide")
		w.refreshQuickMenu(comptes);
		
		// Dire bienvenue !
		Message.send("Bienvenue sur le client FTP");
		
		while(!w.isDisposed()) // Tant que la fenetre principale n'est pas détruite
	      if(!Display.getDefault().readAndDispatch()) // Recuperer et dispatcher les messages
	    	  Display.getDefault().sleep();			  // Se mettre en veille en attendant les prochaines messages

		// Deconnection de tous les sockets
		fullFTPDisconnect();
		
		// Destruction de l'affichage par defaut
		Display.getDefault().dispose();
	}
	
	/* Message print */
	// A chaque type de message sa couleur
	enum MESSAGETYPE {INFO, UP_MSG, DN_MSG, ERROR, NONE};
	Color COLOR_DN_MSG	= new Color(Display.getDefault(), 200, 255, 200);
	Color COLOR_UP_MSG	= new Color(Display.getDefault(), 200, 200, 255);
	Color COLOR_ERROR	= new Color(Display.getDefault(), 255, 100, 100);
	Color COLOR_INFO	= new Color(Display.getDefault(), 225, 225, 225);
	Color COLOR_NONE	= new Color(Display.getDefault(), 255, 255, 255);
	
	// Semaphore pour pas afficher 2 messages en meme temps (risque de probleme au niveau des couleurs de fond)
	// Utiliser "synchronized" ne suffirait pas, car on modifie un composant via une fonction asynchrone (securite qui bloque les acces inter-thread au composant)
	Semaphore s = new Semaphore(0);
	public void printMessage(final String texte, final MESSAGETYPE type){
		if(texte == null) return;
		
		// La securite en question fait que l'on doit modifier un composant en passant par là-dedans
		Display.getDefault().asyncExec(new Runnable() {
            public void run() {
            	try{
            		// Acquisition du semaphore
            		s.acquire();
            		
            		// On recupere la zone de texte de la fenetre principale
            		StyledText trace = w.getTrace();
            		
            		// On compte le nombre de ligne avant l'ajout du message
            		int lc = trace.getLineCount();
            		
            		// Si c'est la commande FTP de mot de passe, on affiche pas le mot de passe par principe de securite
            		if(texte.startsWith("PASS ")) trace.append("PASS *****\n");
            		// Sinon on affiche simplement le message
            		else trace.append(texte+"\n");
					
            		// On y associant sa couleur
				    switch(type){
				    	case DN_MSG: w.getTrace().setLineBackground(lc-1, trace.getLineCount()-lc, COLOR_DN_MSG); break;
				    	case UP_MSG: w.getTrace().setLineBackground(lc-1, trace.getLineCount()-lc, COLOR_UP_MSG); break;
				    	case ERROR:	 w.getTrace().setLineBackground(lc-1, trace.getLineCount()-lc, COLOR_ERROR); break;
				    	case INFO:	 w.getTrace().setLineBackground(lc-1, trace.getLineCount()-lc, COLOR_INFO); break;
				    	default: 	 w.getTrace().setLineBackground(lc-1, trace.getLineCount()-lc, COLOR_NONE); break;
				    }
				    
				    // Et on defile automatiquement à la fin de la zone de texte
				    trace.setCaretOffset(trace.getText().length());
				    trace.showSelection();
				}catch(Exception e){}
				// Pour finir on libere le semaphore
            	s.release();
            }
            
		});
	}
	
	/* Transferts */
	// Ajout d'un download de fichier (FTP -> local)
	public void addDownload(final FTPFile file){
		if(ftp==null || ftp.getSocket() == null) return;
		final String path = Misc.pathBeautifulizer(w.getCurrentLocalDirectory()+"/");
		new Thread(){ public void run(){
			String dstName = file.getName();
			long offset = 0;
			File f;
			// Si le fichier existe deja
				// Si la taille est inferieur au fichier source, on propose de reprendre le transfert ou il en était
				// Sinon on propose d'ecraser le fichier
				// Et si cela ne va toujours pas, on demande a l'utilisateur si il veut utiliser un autre nom de fichier, puis on recommence la procedure
	        while((f = new File(path+dstName)).exists()){
	        	offset = f.length();
				if(offset > 0 && offset < file.length())
					if(Misc.showYesNo(w.getShell(), "Le fichier existe déjà", "Voulez reprendre le téléchargement de \""+dstName+"\" ?")){
						break;
					}else offset = 0;
				if(offset == 0 || offset >= file.length()){
					offset = 0;
					if(dstName.equals(file.getName()))
						if(Misc.showYesNo(w.getShell(), "Ecraser le fichier existant", "Le fichier \""+dstName+"\" va être écrasé. Voulez vous vraiment continuer ?"))
							break;		
				}
				AskWindow aw = new AskWindow("Choisir le nom du fichier", "Veuillez indiquer le nouveau nom du fichier :", dstName, 0, dstName.lastIndexOf("."));
				if((dstName = aw.Ask()) == null) return;
			}
	        // Creation de la tache, et ajout de celle ci dans la liste des transferts presente dans la fenetre principale
			try {
				final Task tsk = new DownloadTask(sch_dl, ftp, file.getName(), path+dstName, file.getParentPath(), offset);
				Display.getDefault().asyncExec(new Runnable() {
			           public void run() {
			        	   w.addTransfert(tsk);
			           }
			    });
			} catch (Exception e) {}
		}}.start();
	}
	
	// Ajout d'un upload de fichier (local -> FTP), quasi identique à addDownload
	public void addUpload(final File file){
		if(ftp==null || ftp.getSocket() == null) return;
		new Thread(){ public void run(){
			long offset = 0;
	        String dstName = file.getName();
			while( (offset=ftp.fileSize(ftp.currentPath+dstName)) != -1 ){
				if(offset > 0 && offset < file.length())
					if(Misc.showYesNo(w.getShell(), "Le fichier existe déjà", "Voulez reprendre le téléchargement de \""+dstName+"\" ?")){
						break;
					}else offset = 0;
				if(offset == 0 || offset >= file.length()){
					offset = 0;
					if(dstName.equals(file.getName()))
						if(Misc.showYesNo(w.getShell(), "Ecraser le fichier existant", "Le fichier \""+dstName+"\" va être écrasé. Voulez vous vraiment continuer ?"))
							break;		
				}
				AskWindow aw = new AskWindow("Choisir le nom du fichier", "Veuillez indiquer le nouveau nom du fichier :", dstName, 0, dstName.lastIndexOf("."));
				if((dstName = aw.Ask()) == null) return;
			}
			try{
				final Task tsk = new UploadTask(sch_ul, ftp, file.getAbsolutePath(), dstName, ftp.currentPath, offset);
				Display.getDefault().asyncExec(new Runnable() {
		            public void run() {
		            	w.addTransfert(tsk);
		            }
		        });
			} catch (Exception e) {}
		}}.start();
	}
		
	/* Connection Form */
	// Affichage du gestionnaire de connexion
	public void showFTPConnectionManager(){
		// Si la fenetre existe deja, la ramener au premier plan
		if(cf!=null && !cf.isDisposed()){ cf.Activate(); return; }
		// Sinon la creer, puis l'afficher
		new Thread(){
			public void run(){
				cf = new ConnectionForm(comptes);
				// Affichage bloquant
				final FTPInfo fi = cf.Show();
				cf = null;
				// Une fois valider, mettre a jour les connexions enregistrees dans le menu de connexion rapide
				w.refreshQuickMenu(comptes);
				if(fi==null) return;
				// Et si voulu, se connecter au FTP
				connectFTP(fi);
			}
		}.start();
	}
	
	/* FTP Communications */
	// Creer un dossier sur le FTP
	public boolean makeFTPDir(FTPFile file, String path) {
		// Si il n'y a pas de connexion, ne rien faire
		if(ftp==null) return false;
		
		// Indiquer que l'on cree un dossier
		Message.send("Création du dossier \""+path+"\"...");
		
		// Appel de la fonction de creation de dossier via la classe FTP, puis afficher et retourner le resultat
		if(ftp.makeDir(file, path)){
			Message.send("Dossier \""+path+"\" créé.");
			return true;
		}
		Message.sendError("Impossible de créer le dossier \""+path+"\".");
		return false;
	}
	
	public boolean connectFTP(FTPInfo fi){
		try{
			Message.send("Connexion...");
			// Creation puis connexion au FTP via les informations de FTPInfo
			ftp = new FTP();
			ftp.connect(fi);
			// Indiquer a la fenetre qu'on est connecte
			w.Connected();
			// Definir la racine comme repertoire courant
			w.changeRemoteDirectory(null, true);
			Message.send("Connecté.");
			return true;
		} catch (Exception e) {
			Message.sendError("Impossible de se connecter au serveur.");
		}
		return false;
	}
	
	public void fullFTPDisconnect(){
		// On annule toutes les taches en les retirants de leurs ordonnanceurs
		sch_dl.removeAll();
		sch_ul.removeAll();
		
		// Si connexion il y a, la fermer
		if(ftp != null && ftp.getSocket() != null){
			Message.send("Déconnexion...");
			ftp.disconnect();
			ftp = null;
			w.Disconnected();
			Message.send("Déconnecté.");
		}
	}
	
	/* MAIN */
	public static void main(String[] args) {
		new MainFTP();
	}
}
