/*
 *   JSampler - a java front-end for LinuxSampler
 *
 *   Copyright (C) 2005-2007 Grigor Iliev <grigor@grigoriliev.com>
 *
 *   This file is part of JSampler.
 *
 *   JSampler is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License version 2
 *   as published by the Free Software Foundation.
 *
 *   JSampler is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with JSampler; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *   MA  02111-1307  USA
 */

package org.jsampler.view;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.util.Vector;

import javax.swing.SwingUtilities;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;

import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import net.sf.juife.event.TaskEvent;
import net.sf.juife.event.TaskListener;

import org.jsampler.CC;

import org.jsampler.task.InstrumentsDb;

import org.linuxsampler.lscp.DbDirectoryInfo;
import org.linuxsampler.lscp.DbInstrumentInfo;

import org.linuxsampler.lscp.event.InstrumentsDbEvent;
import org.linuxsampler.lscp.event.InstrumentsDbListener;

import static org.linuxsampler.lscp.Parser.*;


/**
 *
 * @author Grigor Iliev
 */
public class InstrumentsDbTreeModel implements TreeModel {
	private DbDirectoryTreeNode root = null;
	private Vector<TreeModelListener> listeners = new Vector<TreeModelListener>();
	
	/**
	 * Creates a new instance of <code>InstrumentsDbTreeModel</code>.
	 */
	public
	InstrumentsDbTreeModel() { this(null); }
	
	/**
	 * Creates a new instance of <code>InstrumentsDbTreeModel</code>.
	 * @param l A listener that will be notified when the root
	 * directory content is loaded.
	 */
	public
	InstrumentsDbTreeModel(final ActionListener l) {
		// TODO: This shouldn't be done in the event-dispatcing thread
		CC.getClient().addInstrumentsDbListener(getHandler());
		///////
				
		initDb(l);
	}
	
	/**
	 * Creates a new instance of <code>InstrumentsDbTreeModel</code>.
	 */
	public
	InstrumentsDbTreeModel(boolean dummy) {
		if(dummy) return;
		// TODO: This shouldn't be done in the event-dispatcing thread
		CC.getClient().addInstrumentsDbListener(getHandler());
		///////
				
		initDb(null);
	}
	
	private void
	initDb(final ActionListener l) {
		final InstrumentsDb.GetDrectory gdi = new InstrumentsDb.GetDrectory("/");
		final InstrumentsDb.GetDrectories gd = new InstrumentsDb.GetDrectories("/");
		final InstrumentsDb.GetInstruments gi = new InstrumentsDb.GetInstruments("/");
		
		gdi.addTaskListener(new TaskListener() {
			public void
			taskPerformed(TaskEvent e) {
				if(gdi.doneWithErrors()) return;
				root = new DbDirectoryTreeNode(gdi.getResult());
				fireNodeStructureChanged(root);
				
				CC.getTaskQueue().add(gd);
				CC.getTaskQueue().add(gi);
			}
		});
		
		gd.addTaskListener(new TaskListener() {
			public void
			taskPerformed(TaskEvent e) {
				root.setConnected(true);
				if(gd.doneWithErrors()) return;
				updateDirectoryContent(root, gd.getResult());
				
				for(int i = 0; i < root.getChildCount(); i++) {
					DbDirectoryTreeNode node = root.getChildAt(i);
					node.setConnected(true);
					updateDirectoryContent(node, "/" + toEscapedFileName(node));
				}
				
				if(l != null) l.actionPerformed(null);
			}
		});
		
		gi.addTaskListener(new TaskListener() {
			public void
			taskPerformed(TaskEvent e) {
				if(gi.doneWithErrors()) return;
				updateDirectoryContent(root, gi.getResult());
			}
		});
		
		CC.getTaskQueue().add(gdi);
	}
	
	public void
	treeWillExpand(TreePath path) {
		DbDirectoryTreeNode node = (DbDirectoryTreeNode)path.getLastPathComponent();
		
		if(!node.isConnected()) {
			node.setConnected(true);
			updateDirectoryContent(node, node.getInfo().getDirectoryPath());
		}
		
		for(int i = 0; i < node.getChildCount(); i++) {
			DbDirectoryTreeNode child = node.getChildAt(i);
			if(child.isConnected()) continue;
			child.setConnected(true);
			String pathName = getPathName(path.getPath());
			if(pathName.length() > 1) pathName += "/";
			updateDirectoryContent(child, pathName + toEscapedFileName(child));
		}
	}
	
	// Tree model methods
	public void
	addTreeModelListener(TreeModelListener l) {
		listeners.add(l);
	}
	
	public void
	removeTreeModelListener(TreeModelListener l) {
		listeners.remove(l);
	}
	
	public Object
	getChild(Object parent, int index) {
		return ((DbDirectoryTreeNode)parent).getChildAt(index);
	}
	
	public int
	getChildCount(Object parent) {
		return ((DbDirectoryTreeNode)parent).getChildCount();
	}
	
	public Object
	getRoot() { return root; }
	
	public int
	getIndexOfChild(Object parent, Object child) {
		if(parent == null || child == null) return -1;
		return ((DbDirectoryTreeNode)parent).getIndex((DbDirectoryTreeNode)child);
	}
	
	public boolean
	isLeaf(Object node) { return ((DbDirectoryTreeNode)node).isLeaf(); }
	
	public void
	valueForPathChanged(TreePath path, Object newValue) {
		
	}
	///////
	
	/**
	 * Schedules an update of the directory content for the specified directory node.
	 */
	private void
	updateDirectoryContent(final DbDirectoryTreeNode dirNode, String dirPath) {
		updateDirectoryContent(dirNode, dirPath, null);
	}
	
	/**
	 * Schedules an update of the directory content for the specified directory node.
	 * @param l A listener that will be notified when the subdirectory list is updated.
	 */
	private void
	updateDirectoryContent (
		final DbDirectoryTreeNode dirNode, String dirPath, final ActionListener l
	) {
		final InstrumentsDb.GetDrectories gd = new InstrumentsDb.GetDrectories(dirPath);
		
		gd.addTaskListener(new TaskListener() {
			public void
			taskPerformed(TaskEvent e) {
				if(gd.doneWithErrors()) {
					if(l != null) l.actionPerformed(null);
					return;
				}
				updateDirectoryContent(dirNode, gd.getResult());
				if(l != null) l.actionPerformed(null);
			}
		});
		CC.scheduleTask(gd);
		
		final InstrumentsDb.GetInstruments gi = new InstrumentsDb.GetInstruments(dirPath);
		
		gi.addTaskListener(new TaskListener() {
			public void
			taskPerformed(TaskEvent e) {
				if(gi.doneWithErrors()) return;
				updateDirectoryContent(dirNode, gi.getResult());
			}
		});
		CC.scheduleTask(gi);
	}
	
	private void
	updateDirectoryContent(DbDirectoryTreeNode parent, DbDirectoryInfo[] children) {
		boolean found = false;
		Vector<DbDirectoryTreeNode> removedNodes = new Vector<DbDirectoryTreeNode>();
		for(int i = 0; i < parent.getChildCount(); i++) {
			for(int j = 0; j < children.length; j++) {
				if(children[j] == null) continue;
				if(children[j].getName().equals(parent.getChildAt(i).toString())) {
					children[j] = null;
					found = true;
					break;
				}
			}
			if(!found) removedNodes.add(parent.getChildAt(i));
			found = false;
		}
		
		for(DbDirectoryTreeNode node : removedNodes) {
			int i = parent.getIndex(node);
			parent.removeDirectory(i);
			fireNodeRemoved(parent, node, i);
		}
		
		for(DbDirectoryInfo info : children) {
			if(info == null) continue;
			DbDirectoryTreeNode node = new DbDirectoryTreeNode(info);
			parent.addDirectory(node);
			fireNodeInserted(node, parent.getIndex(node));
			if(parent.getParent() == null) {
				node.setConnected(true);
				updateDirectoryContent(node, info.getDirectoryPath());
			} else if(parent.isConnected()) {
				updateDirectoryContent(node, info.getDirectoryPath());
			}
		}
	}
	
	private void
	updateDirectoryContent(DbDirectoryTreeNode parent, DbInstrumentInfo[] children) {
		boolean found = false;
		Vector<DbInstrumentInfo> removedNodes = new Vector<DbInstrumentInfo>();
		for(int i = 0; i < parent.getInstrumentCount(); i++) {
			String name = parent.getInstrumentAt(i).getName();
			
			for(int j = 0; j < children.length; j++) {
				if(children[j] == null) continue;
				if(children[j].getName().equals(name)) {
					children[j] = null;
					found = true;
					break;
				}
			}
			if(!found) removedNodes.add(parent.getInstrumentAt(i));
			found = false;
		}
		
		for(DbInstrumentInfo info : removedNodes) {
			int i = parent.getInstrumentIndex(info);
			parent.removeInstrument(i);
		}
		
		for(DbInstrumentInfo info : children) {
			if(info == null) continue;
			parent.addInstrument(info);
		}
	}
	
	/**
	 * Schedules a task for refreshing the directory content of the specified directory.
	 * Note that the specified directory is expected to be connected.
	 * @param dir The absolute path name of the directory to refresh.
	 */
	public void
	refreshDirectoryContent(String dir) {
		final DbDirectoryTreeNode node = getNodeByPath(dir);
		if(node == null) return;
		
		node.removeAllDirectories();
		fireNodeStructureChanged(node);
		node.removeAllInstruments();
		
		final InstrumentsDb.GetDrectories gd = new InstrumentsDb.GetDrectories(dir);
		
		gd.addTaskListener(new TaskListener() {
			public void
			taskPerformed(TaskEvent e) {
				if(gd.doneWithErrors()) return;
				
				for(DbDirectoryInfo info : gd.getResult()) {
					DbDirectoryTreeNode n = new DbDirectoryTreeNode(info);
					node.addDirectory(n);
					fireNodeInserted(n, node.getIndex(n));
					n.setConnected(true);
					updateDirectoryContent(n, n.getInfo().getDirectoryPath());
				}
			}
		});
		CC.scheduleTask(gd);
		
		final InstrumentsDb.GetInstruments gi = new InstrumentsDb.GetInstruments(dir);
		
		gi.addTaskListener(new TaskListener() {
			public void
			taskPerformed(TaskEvent e) {
				if(gi.doneWithErrors()) return;
				
				for(DbInstrumentInfo info : gi.getResult()) {
					node.addInstrument(info);
				}
			}
		});
		CC.scheduleTask(gi);
	}
	
	protected Object[]
	getPathToRoot(DbDirectoryTreeNode node) {
		Vector v = new Vector();
		
		while(node != null) {
			v.insertElementAt(node, 0);
			if(node == getRoot()) break;
			node = node.getParent();
		}
		
		return v.toArray(new Object[v.size()]);
	}
	
	protected String
	getPathName(Object[] objs) {
		if(objs.length == 1) return "/";
		
		StringBuffer sb = new StringBuffer();
		for(int i = 1; i < objs.length; i++) {
			sb.append('/').append(toEscapedFileName(objs[i]));
		}
		
		return sb.toString();
	}
	
	private void
	fireNodeInserted(DbDirectoryTreeNode node, int index) {
		Object[] path = getPathToRoot(node.getParent());
		int[] idxs = { index };
		Object[] objs = { node };
		TreeModelEvent e = new TreeModelEvent(this, path, idxs, objs);
		for(TreeModelListener l : listeners) {
			l.treeNodesInserted(e);
		}
	}
	
	private void
	fireNodeChanged(DbDirectoryTreeNode node, int index) {
		Object[] path = getPathToRoot(node.getParent());
		int[] idxs = { index };
		Object[] objs = { node };
		TreeModelEvent e = new TreeModelEvent(this, path, idxs, objs);
		for(TreeModelListener l : listeners) {
			l.treeNodesChanged(e);
		}
	}
	
	private void
	fireNodeRemoved(DbDirectoryTreeNode parent, DbDirectoryTreeNode node, int index) {
		Object[] path = getPathToRoot(parent);
		int[] idxs = { index };
		Object[] objs = { node };
		TreeModelEvent e = new TreeModelEvent(this, path, idxs, objs);
		for(int i = listeners.size() - 1; i >=0; i--) {
			listeners.get(i).treeNodesRemoved(e);
		}
	}
	
	private void
	fireNodeStructureChanged(DbDirectoryTreeNode node) {
		Object[] path = getPathToRoot(node);
		Object[] objs = { node };
		TreeModelEvent e = new TreeModelEvent(this, path);
		for(TreeModelListener l : listeners) {
			l.treeStructureChanged(e);
		}
	}
	
	public DbDirectoryTreeNode
	getNodeByPath(String path) {
		String[] dirs = getDirectoryList(path);
		if(dirs == null) return null;
		if(dirs.length == 1) return root;
		if(root == null) return null;
		DbDirectoryTreeNode node = root;
		boolean found = false;
		for(int i = 1; i < dirs.length; i++) {
			for(int k = 0; k < node.getChildCount(); k++) {
				String s = toNonEscapedFileName(dirs[i]);
				if(s.equals(node.getChildAt(k).toString())) {
					node = node.getChildAt(k);
					found = true;
					break;
				}
			}
			
			if(!found) return null;
			found = false;
		}
		
		return node;
	}
	
	public String
	getPathByNode(DbDirectoryTreeNode node) {
		if(node == null) return null;
		return getPathName(getPathToRoot(node));
	}
	
	/**
	 * @param l A listener which will be notified when the operation is completed.
	 */
	public void
	loadPath(String path, final ActionListener l) {
		// TODO: This method is lazily implemented. Should be optimized.
		final String[] dirs = getDirectoryList(path);
		if(dirs == null) {
			l.actionPerformed(null);
			return;
		}
		
		final ActionListener listener = new ActionListener() {
			public void
			actionPerformed(ActionEvent e) {
				String s = "";
				DbDirectoryTreeNode node = null;
				for(int i = 0; i < dirs.length; i++) {
					if(i > 1) s += "/" + dirs[i];
					else s += dirs[i];
					node = getNodeByPath(s);
					if(node == null) {
						if(l != null) l.actionPerformed(null);
						return;
					}
					
					if(!node.isConnected()) {
						node.setConnected(true);
						updateDirectoryContent(node, s, this);
						return;
					}
				}
				
				if(l != null) l.actionPerformed(null);
			}
		};
		
		listener.actionPerformed(null);
	}
	
	/** Resets this model. */
	public void
	reset() {
		DbDirectoryTreeNode oldRoot = root;
		root = null;
		TreeModelEvent e = new TreeModelEvent(this, (TreePath)null);
		for(TreeModelListener l : listeners) {
			l.treeStructureChanged(e);
		}
	}
	
	private final EventHandler eventHandler = new EventHandler();
	
	private EventHandler
	getHandler() { return eventHandler; }
	
	private class EventHandler implements InstrumentsDbListener {
		/**
		 * Invoked when the number of instrument
		 * directories in a specific directory has changed.
		 */
		public void
		directoryCountChanged(final InstrumentsDbEvent e) {
			SwingUtilities.invokeLater(new Runnable() {
				public void
				run() { updateDirectoryCount(e); }
			});
		}
		
		private void
		updateDirectoryCount(InstrumentsDbEvent e) {
			if(e.getPathName().equals("/") && root == null) {
				/*
				 * If the directory tree failed to load due to a database
				 * corruption it may work now if the db has been formatted
				 */
				initDb(null);
			}
			
			DbDirectoryTreeNode node = getNodeByPath(e.getPathName());
			if(node == null) return;
			if(!node.isConnected()) return;
			
			updateDirectoryContent(node, e.getPathName());
		}
		
		/**
		 * Invoked when the settings of an instrument directory are changed.
		 */
		public void
		directoryInfoChanged(final InstrumentsDbEvent e) {
			SwingUtilities.invokeLater(new Runnable() {
				public void
				run() { updateDirectoryInfo(e); }
			});
		}
		
		private void
		updateDirectoryInfo(InstrumentsDbEvent e) {
			final DbDirectoryTreeNode node = getNodeByPath(e.getPathName());
			if(node == null) return;
			
			final InstrumentsDb.GetDrectory t =
				new InstrumentsDb.GetDrectory(e.getPathName());
			
			t.addTaskListener(new TaskListener() {
				public void
				taskPerformed(TaskEvent e) {
					if(t.doneWithErrors()) return;
					if(node.getParent() != null) {
						node.getParent().updateDirectory(t.getResult());
						fireNodeChanged(node, node.getParent().getIndex(node));
					}
				}
			});
			
			CC.getTaskQueue().add(t);
		}
		
		/**
		 * Invoked when an instrument directory is renamed.
		 */
		public void
		directoryNameChanged(final InstrumentsDbEvent e) {
			SwingUtilities.invokeLater(new Runnable() {
				public void
				run() { directoryRenamed(e); }
			});
		}
		
		private void
		directoryRenamed(InstrumentsDbEvent e) {
			DbDirectoryTreeNode node = getNodeByPath(e.getPathName());
			if(node == null) {
				// If the directory is renamed by this frontend the
				// directory should already be with the new name
				String s = getParentDirectory(e.getPathName());
				if(s.length() == 1) s += toEscapedFileName(e.getNewName());
				else s += "/" + toEscapedFileName(e.getNewName());
				node = getNodeByPath(s);
			}
			if(node == null || node.getParent() == null) {
				CC.getLogger().warning("Invalid path: " + e.getPathName());
				return;
			}
			
			node.setName(e.getNewName());
			DbDirectoryTreeNode parent = node.getParent();
			
			int i = parent.getIndex(node);
			parent.removeDirectory(i);
			fireNodeRemoved(parent, node, i);
			
			parent.addDirectory(node);
			fireNodeInserted(node, parent.getIndex(node));
		}
		
		/**
		 * Invoked when the number of instruments
		 * in a specific directory has changed.
		 */
		public void
		instrumentCountChanged(final InstrumentsDbEvent e) {
			SwingUtilities.invokeLater(new Runnable() {
				public void
				run() {
					DbDirectoryTreeNode node = getNodeByPath(e.getPathName());
					if(node == null) {
						return;
					}
					if(!node.isConnected()) {
						return;
					}
					
					updateDirectoryContent(node, e.getPathName());
				}
			});
		}
		
		/**
		 * Invoked when the settings of an instrument are changed.
		 */
		public void
		instrumentInfoChanged(final InstrumentsDbEvent e) {
			SwingUtilities.invokeLater(new Runnable() {
				public void
				run() { updateInstrumentInfo(e); }
			});
		}
		
		/**
		 * Invoked when an instrument is renamed.
		 */
		public void
		instrumentNameChanged(final InstrumentsDbEvent e) {
			SwingUtilities.invokeLater(new Runnable() {
				public void
				run() { instrumentRenamed(e); }
			});
		}
		
		private void
		updateInstrumentInfo(InstrumentsDbEvent e) {
			String dir = getParentDirectory(e.getPathName());
			final DbDirectoryTreeNode node = getNodeByPath(dir);
			if(node == null) return;
			if(!node.isConnected()) return;
			
			final InstrumentsDb.GetInstrument t =
				new InstrumentsDb.GetInstrument(e.getPathName());
			
			t.addTaskListener(new TaskListener() {
				public void
				taskPerformed(TaskEvent e) {
					if(t.doneWithErrors()) return;
					node.updateInstrument(t.getResult());
				}
			});
			
			CC.getTaskQueue().add(t);
		}
		
		private void
		instrumentRenamed(InstrumentsDbEvent e) {
			String dir = getParentDirectory(e.getPathName());
			DbDirectoryTreeNode node = getNodeByPath(dir);
			if(node == null) return;
			
			String instr = getFileName(e.getPathName());
			if(instr == null) return;
			
			DbInstrumentInfo info = node.getInstrument(toNonEscapedFileName(instr));
			
			if(info == null) {
				// If the instrument is renamed by this frontend the
				// instrument should already be with the new name
				info = node.getInstrument(e.getNewName());
			}
			if(info == null) {
				CC.getLogger().warning("Invalid path: " + e.getPathName());
				return;
			}
			
			info.setName(e.getNewName());
			node.removeInstrument(node.getInstrumentIndex(info));
			node.addInstrument(info);
		}
		
		/** Invoked when the status of particular job has changed. */
		public void
		jobStatusChanged(InstrumentsDbEvent e) { }
	}
}
