package ftp_client;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.TreeEditor;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

// Element de l'arbre qui affiche les fichiers, herite de TreeItem
// Cette classe a pour but de simplifier les elements en liant l'item a un fichier,
// lui permette de gerer lui meme l'icone du fichier,
// ainsi que quelques actions sur le fichier,
// et de gerer lui même son menu contextuel
public class FileTreeItem extends TreeItem {

	/* Variables */
	public File file = null; // Fichier lie (un dossier est considere comme un fichier)
	Text txt = null;		 // Zone de saisie en cas de renommage
	Shell shell;			 // Shell parent
	
	boolean isRoot = false;	 // S'agit-il d'un lecteur (Windows) ou de la racine des repertoires (Linux)
	
	/* Constructors */
	// Recuperation du Shell parent, de l'element parent, et du texte par defaut
	public FileTreeItem(Shell shell, TreeItem parent, String[] texts){
		super(parent, SWT.NONE);
		this.setText(texts);
		this.shell = shell;
		init();
	}
	// Recuperation du Shell parent, de l'arbre parent (au cas ou cet element soit une des racines de l'arbre), et du texte par defaut
	public FileTreeItem(Shell shell, Tree parent, String[] texts){
		super(parent, SWT.NONE);
		this.setText(texts);
		this.shell = shell;
		init();
	}
	// Recuperation du Shell parent, de l'element parent, et du fichier a lier
	public FileTreeItem(Shell shell, TreeItem parent, File file){
		super(parent, SWT.NONE);
		this.file = file;
		this.setText(file.getName());
		this.shell = shell;
		init();
	}
	// Recuperation du Shell parent, de l'arbre parent (au cas ou cet element soit une des racines de l'arbre), et du fichier a lier
	public FileTreeItem(Shell shell, Tree parent, File file){
		super(parent, SWT.NONE);
		this.file = file;
		this.setText(file.getName());
		this.shell = shell;
		init();
	}
	// Recuperation du Shell parent, de l'arbre parent (au cas ou cet element soit une des racines de l'arbre), du fichier a lier et si c'est une racine
	public FileTreeItem(Shell shell, Tree parent, File file, boolean isRoot){
		super(parent, SWT.NONE);
		this.file = file;
		this.isRoot = isRoot;
		if(isRoot) this.setText(file.getAbsolutePath());
		else this.setText(file.getName());
		this.shell = shell;
		init();
	}
	
	// Initialisation
	void init(){
		// On indique a ceux qui veulent qu'on a ete cree
		fireItemsAddedListener(this);
		
		// Lors de la destruction
		this.addDisposeListener(new DisposeListener() {
			public void widgetDisposed(DisposeEvent e) {
				if(txt!=null){txt.dispose(); txt = null;} // On detruit la zone de saisie
				if(FileTreeItem.this.getImage()!=null) FileTreeItem.this.getImage().dispose(); // On detruit l'image
				fileTreeListeners.clear(); // On efface les ecouteurs pour les actions de fichier
			}
	    });
		
		// Si il y a un element parent (si on est pas une racine donc), on reprend sa couleur
		if(this.getParentItem() != null) this.setBackground(this.getParentItem().getBackground());
		
		// On met a jour l'icone
		updateIcon();
	}
	
	/* Update file icon */
	// Mise a jour de l'icone du fichier
	void updateIcon(){
		// Si il n'y a pas de fichier, on arrete evidement
		if(file==null) return;
		
		// Si il s'agit d'un dossier
		if(file.isDirectory()){
			// On indique le nom du dossier, sa date de modication et son possesseur
			this.setText(new String[] { (isRoot?file.getAbsolutePath():file.getName()), "(dossier)", Misc.getDate(file), Misc.getOwner(file) });
			// On recupere l'icone systeme du repertoire (icone specifique par exemple)
			Image img = Misc.getSystemImageFromFile(file);
			// Si l'image est null, alors on reprend l'icone systeme du repertoire courant
			if(img == null) img = Misc.getSystemImageFromFile(new File("."));
			// On supprime bien l'ancienne image
			if(this.getImage() != null) this.getImage().dispose();
			// On applique l'image
			this.setImage(img);
		}else{
		// Si il s'agit d'un fichier
			// On indique le nom du fichier, sa taille, sa date de modification et son possesseur
			this.setText(new String[] { file.getName(), Misc.sizeToString(file.length()), Misc.getDate(file), Misc.getOwner(file) });
			// On supprime bien l'ancienne image
			if(this.getImage() != null) this.getImage().dispose();
			// Meme principe que pour les dossiers
			/*Image img = Misc.getSystemImageFromFile(file); // Commente car n'apporte pas grand chose par rapport aux ressources demandees
			if(img!=null) this.setImage(img);
			else */this.setImage(Misc.getIconFromName((isRoot?file.getName():file.getAbsolutePath())));
		}
	}
	
	/* Listeners */
	// Lorsque que l'on double clique sur l'element
	void dbleClick(){
		// Si un fichier est lie, et qu'il ne s'agit pas d'un dossier, demander le transfert
		if(file!=null && !file.isDirectory()) fireTransfertListener(file);
	}
	// Lorsque que l'on clique droit
	void rightClick(final MainWindow sender, final MouseEvent e){
		// Si aucun fichier n'est lie, alors ne rien faire
		if(file == null) return;
		
		// Sinon, generer un menu contextuel, avec telles ou telles options si il s'agit d'un dossier ou non
		Menu mn = new Menu(this.getParent());
		MenuItem mi;
		
		if(file.isDirectory()){
			mi = new MenuItem(mn, SWT.PUSH);		
			mi.setText("Selectionner (Rafraîchir)");
			// Quand on clique sur cette option
			mi.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					// On demande a nous selectionner/rafraichir
					fireRefreshSubItemsListener();
				}
			});	
		}else{
			mi = new MenuItem(mn, SWT.PUSH);		
			mi.setText("Transferer");
			mi.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					// On demande a gerer le transfert de notre fichier
					fireTransfertListener(file);
				}
			});
			mi = new MenuItem(mn, SWT.PUSH);		
			mi.setText("Selectionner (Rafraîchir)");
			mi.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					// Si ce n'est pas une racine, selectionner/rafraichir les sous-elements
					if(FileTreeItem.this.getParentItem() != null)
            			((FileTreeItem)FileTreeItem.this.getParentItem()).fireRefreshSubItemsListener();
					// Sinon ce n'est pas une racine, selectionner/rafraichir la racine de l'arbre
            		else fireRefreshTreeListener(FileTreeItem.this.getParent());
				}
			});	
		}
		mi = new MenuItem(mn, SWT.PUSH);		
		mi.setText("Nouveau dossier");
		mi.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() {
			public void widgetSelected(SelectionEvent event) {
				// Procedure de creation d'un nouveau dossier
				new Thread(){
					public void run(){
						// On demande le nom du dossier via AskWindow
						AskWindow aw = new AskWindow("Nouveau dossier", "Veuillez indiquer le nom du nouveau dossier :", "Nouveau Dossier");
						String name = aw.Ask();
						// Si l'utilisateur a annule, on arrete la procedure
						if(name==null) return;
						// On demande a creer le dossier, si tout le monde est ok (si tous ceux qui sont abonnes (ici que MainFTP) ont reussi et retourne vrai)
						if(fireNewFolderListener(name))
							Display.getDefault().asyncExec(new Runnable() {
					            public void run() {
					            	// Si on est dans un dossier (et donc que le nouveau dossier se trouve dans ce dossier)
					            	if(file.isDirectory()){
					            		// Si le dossier est ouvert, selectionner/rafraichir les sous-elements
					            		if(FileTreeItem.this.getExpanded()) fireRefreshSubItemsListener();
					            	}else{
					            	// Sinon rafraichir le parent
					            		if(FileTreeItem.this.getParentItem() != null)
					            			((FileTreeItem)FileTreeItem.this.getParentItem()).fireRefreshSubItemsListener();
					            		else fireRefreshTreeListener(FileTreeItem.this.getParent());
					            	}
					            }
							});
					}
				}.start();
			}
		});
		// Si on est pas une racine
		if(!isRoot){
			mi = new MenuItem(mn, SWT.PUSH);
			mi.setText("Renommer");
			mi.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					// Procedure de renommage
					renameProcess(sender);
				}
			});
			
			mi = new MenuItem(mn, SWT.PUSH);
			mi.setText("Effacer");
			mi.addSelectionListener(new org.eclipse.swt.events.SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					// Procedure d'effacement
					deleteProcess();
				}
			});
		}
		
		// Deplacer le menu contextuel a l'endroit du clique et l'afficherr
		Point pt = new Point (e.x, e.y); 	 // Recuperation du point de clique par rapport a l'element
		pt = this.getParent().toDisplay(pt); // Resituer le point par rapport a l'ecran
		mn.setLocation (pt.x, pt.y);		 // Deplacer le menu
		mn.setVisible (true);				 // L'afficher
	}
	
	// Raccourcis clavier
	public void keyDown(MainWindow sender, KeyEvent e) {
		switch(e.keyCode){
			case SWT.F2	 : renameProcess(sender);	break;								// F2 pour renommer
			case SWT.F5  : if(this.getExpanded()) fireRefreshSubItemsListener(); break;	// F5 pour selectionner/rafraichir
			case SWT.CR  : dbleClick();				break;								// "Entree" equivaut a double-cliquer
			case SWT.DEL : deleteProcess();			break;								// "Suppr" pour effacer
		}
	}
	
	/* Delete process */
	// Procedure d'effacement de fichier utilisateur (avec confirmation)
	void deleteProcess(){
		if(isRoot) return; // Si il s'agit d'une racine (d'un lecteur par exemple), on peut bien evidement pas l'effacer
		// On demande une confirmation, si oui, effacer le fichier
		if(Misc.showYesNo(shell, "Effacer un élément", "Êtes-vous sur de vouloir effacer \""+this.file.getName()+"\" ?"))
			deleteFile();
	}
	// Procedure d'effacement de fichier
	public void deleteFile(){
		// Effacement de maniere asynchrone (si il s'agit fichier FTP, cela risque de prendre quelques secondes)
		new Thread(){
			public void run(){
				Message.send("Suppression de \""+(isRoot?file.getName():file.getAbsolutePath())+"\"...");
				
				// On demande au fichier de s'effacer
				final boolean ok = file.delete();
				
				Display.getDefault().asyncExec(new Runnable() {
		            public void run() {
		            	if(ok){ // Si l'effacement s'est bien deroule, effacer l'item
		            		FileTreeItem.this.dispose();
		            		Message.send("Fichier \""+(isRoot?file.getName():file.getAbsolutePath())+"\" supprimé.");
		            	}		// Sinon afficher une erreur
		            	else Message.sendError("Impossible de supprimer \""+(isRoot?file.getName():file.getAbsolutePath())+"\".");
		            }
				});
			}
		}.start();
	}
	
	/* Rename process */
	// Procedure de renommage de fichier utilisateur (avec zone de saisie)
	void renameProcess(final MainWindow sender){
		if(isRoot) return;		// On ne renomme pas la racine
		if(txt != null) return; // Si on est deja en train de renommer, on quitte
		
		// Initialisation de la zone de saisie...
		txt = new Text(this.getParent(), SWT.NONE);
		// ...qu'on s'apprete a lier a l'element par rapport a sa position et a sa taille...
		TreeEditor te = new TreeEditor(this.getParent());
		te.grabHorizontal = te.grabVertical = true; // ...en largeur et en hauteur
		txt.setText(this.getText()); // Le texte de la zone de saisie est le nom du fichier
		// On selectionne le nom sans l'extension (plus simple pour l'utilisateur)
		int lastdot = -1;
		if(this.getItemCount() == 0) lastdot = this.getText().lastIndexOf(".");
		if(lastdot == -1) txt.selectAll();
		else txt.setSelection(0, lastdot);
		// On active la zone de saisie pour evite a l'utilisateur de cliquer dessus pour pouvoir modifier le texte
		txt.setFocus();
		// Raccourcis clavier
		txt.addKeyListener(new org.eclipse.swt.events.KeyAdapter() {
			public void keyPressed(org.eclipse.swt.events.KeyEvent e) {
				if(e.character == SWT.CR){ // "Entree" valide
					e.doit = false; // On rajoute pas le caractere "Entree" a la zone de saisie
					renameFile(sender, txt.getText()); // Vrai procedure de renommage
					txt.dispose(); // On detruit la zone de saisie
					txt = null;
				}else if(e.character == SWT.ESC){ // "Echape" annule
					e.doit = false; // On rajoute pas le caractere "Echape" a la zone de saisie
					txt.dispose();  // On detruit la zone de saisie
					txt = null;
				}
			}
		});
		// Quand la zone de saisie perd le focus, cela valide automatiquement
		txt.addFocusListener(new org.eclipse.swt.events.FocusAdapter() {
			public void focusLost(org.eclipse.swt.events.FocusEvent e) {
				renameFile(sender, txt.getText());
				txt.dispose();
				txt = null;
			}
		});
		// Liaison de la zone de saisie a l'element
        te.setEditor(txt, this);
	}
	// Procedure de renommage de fichier
	public void renameFile(final MainWindow sender, final String newname){
		new Thread(){ // Meme principe que pour l'effacement d'un fichier
			public void run(){
				boolean isok = false;
				final String oldname = file.getName();
				Message.send("Renommage de \""+oldname+"\" en \""+newname+"\"...");
				try{
					// On renomme un fichier en lui donnant comme un argument un File avec le nouveau nom
					File nf = new File(Misc.getParentPath(file)+File.separator+newname);
					isok = file.renameTo(nf);
					// Si le fichier a bien ete renomme, mais que le nom ne s'est pas mis a jour (cas pour la classe FILE de base qui se met pas a jour automatiquement..)
					if(isok && !file.getName().equals(nf.getName())) file = nf; // On remplace le fichier lie par le nouveau
				}catch(SecurityException ex){}
				
				// Si il s'agissait d'un repertoire, informer la fenetre principale de metter a jour l'arbre, etc.
				if(file.isDirectory()) sender.directoryRenamed(FileTreeItem.this);
				
				final boolean ok = isok;
				Display.getDefault().asyncExec(new Runnable() {
		            public void run() {
		            	if(ok){ // Si ca s'est bien passe, modifier le nom de l'element, mettre a jour l'icone (en cas de modification de l'extension)
		            		FileTreeItem.this.setText(0, newname);
		            		updateIcon();
		            		Message.send("Fichier \""+oldname+"\" renommé en \""+newname+"\".");
		            	}else{ // Sinon afficher une erreur
		            		Message.sendError("Impossible de renommer \""+file.getName()+"\" en \""+newname+"\".");
		            	}
		            }
				});
			}
		}.start();
	}
	
	/* Change background */
	// Changer la couleur de fond de l'element et des sous-elements
	public void setGlobalBackground(Color clr){
		this.setBackground(clr); // On change la couleur de fond de l'element
		for(TreeItem ti : this.getItems()) // ...et des sous-elements
			ti.setBackground(clr);
	}
	
	/* Get the (parent) path */
	// Recuperer le chemin du fichier (FullPath = true) ou du dossier parent (FullPath = false)
	public String getPath(boolean FullPath){		
		if(file == null) return "";
		if(FullPath) return file.getAbsolutePath();
		else return Misc.getParentPath(file); // file.getParentFile() pas gere dans FTPFile
	}
	
	/* Communicate with the main class */
	// Ecouteurs pour...
	public interface FileTreeListener {
	  void addTansfert(File file); // ...ajouter un transfert
	  boolean newFolder(FileTreeItem sender, String name); // ...creer un nouveau dossier
	  void refreshSubItems(FileTreeItem sender); // ...rafraichir les sous-elements
	  void refreshTree(Tree tree);	// ...rafraichir l'arbre
	}	

	private final Collection<FileTreeListener> fileTreeListeners	= new ArrayList<FileTreeListener>();

	protected boolean fireNewFolderListener(String name) {
		// Si tout le monde reussi, retourner vrai
		boolean allOk = true;
		for(FileTreeListener listener : fileTreeListeners)
			if(!listener.newFolder(this, name)) allOk = false;
		return allOk;
	}

	protected void fireTransfertListener(final File file) {
		for(FileTreeListener listener : fileTreeListeners)
			listener.addTansfert(file);
	}

	public void fireRefreshTreeListener(Tree tree) {
		  for(FileTreeListener listener : fileTreeListeners)
			   listener.refreshTree(tree);
	}
	
	public void fireRefreshSubItemsListener() {
	  for(FileTreeListener listener : fileTreeListeners)
		   listener.refreshSubItems(this);
	}

	public void addTransfertListener(FileTreeListener listener) {
	  fileTreeListeners.add(listener);
	}

	public void removeTransfertListener(FileTreeListener listener) {
	  fileTreeListeners.remove(listener);
	}

	
	/* Link every FileTreeItem with the main class */
	// Ecouteur declenche a chaque creation d'element (ce qui permet au coeur du programme de gerer l'ecouteur au dessus pour tous les elements)
	public interface AddListener {
	  void itemAdded(FileTreeItem sender);
	}	

	private static final Collection<AddListener> addListeners	= new ArrayList<AddListener>();

	protected static void fireItemsAddedListener(final FileTreeItem me) {
	  for(AddListener listener : addListeners)
		   listener.itemAdded(me);
	}
	  
	public static void addItemsAddedListener(AddListener listener) {
	  addListeners.add(listener);
	}

	public static void removeItemsAddedListener(AddListener listener) {
	  addListeners.remove(listener);
	}

	/* Subclassing not allowed in SWT */
	// Oblige sinon SWT lance une exception, SWT refuse l'heritage...
	protected void checkSubclass() {}
}
