// FileTv.cs created with MonoDevelop
// User: klose at 13:47 15.01.2009
// CVS release: $Id: FileTv.cs,v 1.32 2011-04-19 12:36:58 klose Exp $
//
//    QuickStep: Acquire, view and identify spectra 
//    Copyright (C) 2009  Thomas Klose, Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.IO;
using System.Text.RegularExpressions;
using Gtk;

using Hiperscan.SGS;
using Hiperscan.Spectroscopy;
using Hiperscan.FileIO;
using Hiperscan.GUI.Figure;
using Hiperscan.Unix;


namespace Hiperscan.GUI
{

	public class SpectrumCache
	{
		private Dictionary<string,Spectrum> spectra;
		int max_count;
		
		public SpectrumCache(int max_count)
		{
			this.max_count = max_count;
			this.spectra   = new Dictionary<string,Spectrum>();
		}
		
		public void Add(Spectrum spectrum)
		{
			if (this.spectra.ContainsKey(spectrum.FileInfo.FullName))
			{
				this.spectra[spectrum.FileInfo.FullName] = spectrum;
			}
			else
			{
				if (this.spectra.Count+1 > this.max_count)
					this.spectra.Remove((new List<string>(this.spectra.Keys))[0]);

				this.spectra.Add(spectrum.FileInfo.FullName, spectrum);
				Console.WriteLine("Added spectrum to cache. New cache count: " + this.spectra.Count);
				Console.WriteLine(spectrum.FileInfo.FullName);
			}
		}
		
		public void Remove(string fname)
		{
			if (this.spectra.ContainsKey(fname))
			    this.spectra.Remove(fname);
		}
		
		public Spectrum Get(string fname)
		{
			if (this.spectra.ContainsKey(fname))
				return this.spectra[fname];

			Spectrum spectrum = CSV.Read(fname);
			this.Add(spectrum);
			
			return spectrum;
		}
		
		public void Clear()
		{
			this.spectra.Clear();
		}
	}
	
	
	internal class FileTv : Gtk.TreeView
	{
		internal static SpectrumCache Cache = new SpectrumCache(100);
		internal static SpectrumTooltip Tooltip;

		
		public delegate void ReturnPressedHandler(FileTv tv);
		public event         ReturnPressedHandler ReturnPressed;

		public delegate void DeletePressedHandler(FileTv tv);
		public event         DeletePressedHandler DeletePressed;

		public delegate void DoubleClickedHandler(FileTv tv);
		public event         DoubleClickedHandler DoubleClicked;

		public delegate void Button1ClickedHandler(FileTv tv);
		public event         Button1ClickedHandler Button1Clicked;
		
		public delegate void Button3ClickedHandler(FileTv tv);
		public event         Button3ClickedHandler Button3Clicked;
		
		public delegate void MotionHandler(object o, Gdk.EventMotion e);
		public event         MotionHandler Motion;
		
		public delegate void LeftHandler(object o, Gdk.EventCrossing e);
		public event         LeftHandler Left;
		
		
		public FileTv(TreeStore store) : base(store) {}

		protected override bool OnKeyPressEvent(Gdk.EventKey e)
		{
			if ((e.Key == Gdk.Key.Return || e.Key == Gdk.Key.KP_Enter) &&  (this.ReturnPressed != null))
				this.ReturnPressed(this);
			
			if ((e.Key == Gdk.Key.Delete) &&  (this.DeletePressed != null))
				this.DeletePressed(this);
			
			return base.OnKeyPressEvent(e);
		}
		
		protected override bool OnButtonPressEvent(Gdk.EventButton e)
		{
			if (e.Button == 1)
                this.Button1Clicked?.Invoke(this);

			if (e.Button == 3)
                this.Button3Clicked?.Invoke(this);

			if (e.Button == 1 && e.Type == Gdk.EventType.TwoButtonPress)
                this.DoubleClicked?.Invoke(this);
			
			return base.OnButtonPressEvent(e);
		}
		
		protected override bool OnMotionNotifyEvent(Gdk.EventMotion e)
		{
			if (this.Motion != null)
				this.Motion(this, e);
			
			return base.OnMotionNotifyEvent(e);
		}
		
		protected override bool OnLeaveNotifyEvent(Gdk.EventCrossing e)
		{
			if (this.Left != null)
				this.Left(this, e);
			
			return base.OnLeaveNotifyEvent(e);
		}
	}
	
	public class CloseButton : Gtk.Button
	{
		
		public CloseButton() : base()
		{
			this.Relief      = ReliefStyle.None;
			this.TooltipText = Catalog.GetString("Close");
			this.Image = new Image(MainClass.CloseIcons[0]);

			this.ModifyStyle();
			
			this.StyleSet += delegate(object o, StyleSetArgs args) {
				this.ModifyStyle();
			};
		}
		
		private void ModifyStyle()
		{
			if (this.Style.XThickness == 0 && this.Style.YThickness == 0)
				return;
			RcStyle style = new RcStyle();
			style.Xthickness = 0;
			style.Ythickness = 0;
			this.ModifyStyle(style);
		}
	}
		
	public class ScrolledFileTv : Gtk.VBox
	{
		private enum Store : int
		{
			Filename,
			Serial,
			Mode,
			Path,
			IsSpectrum,
			DirectoryProperty
		}

		private enum FsWatcherEventType
		{
			Created,
			Changed,
			Deleted
		}
		
		private class FsWatcherEvent
		{
			private FsWatcherEventType type;
			private Spectrum spectrum;
			private string full_path;
			
			public FsWatcherEvent(FsWatcherEventType type, Spectrum spectrum)
			{
				this.type      = type;
				this.spectrum  = spectrum;
				this.full_path = string.Empty;
			}
			
			public FsWatcherEvent(FsWatcherEventType type, string full_path)
			{
				this.type      = type;
				this.spectrum  = null;
				this.full_path = full_path;
			}
			
			public FsWatcherEventType Type
			{
				get { return this.type; }
			}

			public bool IsSpectrum
			{
				get
				{
					if (this.spectrum != null)
						return true;
					else
						return false;
				}
			}
			
			public Spectrum Spectrum
			{
				get { return this.spectrum; }
			}
			
			public string FullPath
			{
				get
				{
					if (this.spectrum != null)
						return this.spectrum.FileInfo.FullName;
					else
						return this.full_path;
				}
			}
		}
		
		private class FilterEntry : Gtk.Entry
		{
			private Regex regex = null;
			private Gdk.Color error_color;
			private Gdk.Color standard_color;
			
			public delegate void RegexChangedHandler(Regex regex);
			public event RegexChangedHandler RegexChanged;
			
			
			public FilterEntry(Gdk.Color error_color) : base()
			{
				this.error_color = error_color;
				this.Changed += delegate { this.UpdateRegexp(); };
				this.standard_color = this.Style.Base(StateType.Normal);
			}
			
			private void UpdateRegexp()
			{
				bool changed  = false;
				bool was_null = this.regex == null;
				
				try
				{
					this.regex = new Regex(this.Text, RegexOptions.IgnoreCase);
					this.ModifyBase(StateType.Normal, this.standard_color);
					changed = true;
				}
				catch (ArgumentException)
				{
					this.regex = null;
					this.ModifyBase(StateType.Normal, this.error_color);
					if (!was_null)
						changed = true;
				}
				
				if (changed && this.RegexChanged != null)
					this.RegexChanged(this.regex);
			}
			
			public Regex Regex
			{
				get { return this.regex; }
			}
		}

		private Queue fs_event_queue = Queue.Synchronized(new Queue()); 
		
		private FilterEntry       filter_entry;
		private FileTv            tv;
		private TreeStore         store;
		private FileSystemWatcher fs_watcher;
		
		private string directory_path;
		private TreeModelFilter filter;
		private Button clear_filter_button;
		private Button open_all_button;

		private volatile bool __close_request = false;
		
		private readonly string file_mask = "*.csv";
		
		private ArrayList      columns = new ArrayList();
		//private List<TreePath> _paths  = new List<TreePath>();
		private List<TreeIter> _iters  = new List<TreeIter>();
		
		private TreePath current_tooltip_path = null;
		
		public delegate void SpectrumSelectionChangedHandler(List<Spectrum> spectra);
		public event         SpectrumSelectionChangedHandler SpectrumSelectionChanged;
		
		public delegate void DirectorySelectionChangedHandler(List<DirectoryProperty> dirs);
		public event         DirectorySelectionChangedHandler DirectorySelectionChanged;
		
		public delegate void SpectrumDoubleClickedHandler(Spectrum spectrum);
		public event         SpectrumDoubleClickedHandler SpectrumDoubleClicked;

		public delegate void DirectoryDoubleClickedHandler(DirectoryProperty dir);
		public event         DirectoryDoubleClickedHandler DirectoryDoubleClicked;
		
		public delegate void OpenAllClickedHandler(List<Spectrum> spectra);
		public event         OpenAllClickedHandler OpenAllClicked;

		public delegate void ClosedHandler();
		public event         ClosedHandler Closed;

		
		public ScrolledFileTv(string path) : base()
		{
			this.directory_path = path;

			HBox hbox = new HBox();
			hbox.PackStart(new Label(Catalog.GetString("Filter:")), false, false, 3);
			
			this.store = new TreeStore(typeof(string),             // filename
			                           typeof(string),             // serial
			                           typeof(CellRendererMode),   // renderer mode
			                           typeof(string),             // file or dir path
			                           typeof(bool),               // is spectrum?
			                           typeof(DirectoryProperty)); // directory property
			
			this.filter = new TreeModelFilter(this.store, null);
			this.filter.VisibleFunc = new TreeModelFilterVisibleFunc(this.FilterTree);
			
			this.filter_entry = new FilterEntry(UserInterface.entry_error_color);
			this.filter_entry.WidthChars = 20;
			this.filter_entry.TooltipText = Catalog.GetString("Regular expression for filename or serial number");
			this.filter_entry.RegexChanged += delegate {
				this.filter.Refilter();
				this.clear_filter_button.Sensitive = !string.IsNullOrEmpty(this.filter_entry.Text);
			};
			hbox.PackStart(this.filter_entry, false, false, 3);

			this.clear_filter_button = new Button(new Image(Stock.Clear, IconSize.Menu));
			this.clear_filter_button.TooltipText = Catalog.GetString("Clear regular expression");
			this.clear_filter_button.Relief = ReliefStyle.None;
			this.clear_filter_button.Sensitive = false;
			this.clear_filter_button.Clicked += delegate { this.filter_entry.Text = string.Empty; };
			Alignment align = new Alignment(0f, 0.5f, 0f, 0f);
			align.Add(this.clear_filter_button);
			hbox.PackStart(align, false, false, 0);
			
			this.open_all_button = new Button(new Image(Stock.Open, IconSize.Menu));
			this.open_all_button.TooltipText = Catalog.GetString("Open all");
			this.open_all_button.Relief = ReliefStyle.None;
			this.open_all_button.Sensitive = false;
			this.open_all_button.Clicked += delegate {
				if (this.OpenAllClicked != null)
					this.OpenAllClicked(this.GetAllSpectra());
			};
			align = new Alignment(0f, 0.5f, 0f, 0f);
			align.Add(this.open_all_button);
			hbox.PackStart(align, true, true, 3);
			
			this.tv = new FileTv(store);
			
			this.tv.WidthRequest  = 210;
			this.tv.HeightRequest = 120;
			
			this.tv.RulesHint = false;
			this.tv.Selection.Mode = SelectionMode.Single;
			
			this.tv.Motion            += new FileTv.MotionHandler(this.OnMotion);
			this.tv.Left              += new FileTv.LeftHandler(this.OnLeft);
			this.tv.Selection.Changed += new EventHandler(this.OnSelectionChanged);
			
			CellRendererText text;
			TreeViewColumn   column;
				
			// filename column
			text = new CellRendererText();
			text.Xalign = 0f;
			this.columns.Add(text);
			column = new TreeViewColumn(Catalog.GetString("Spectrum file"), text,
			                            "markup", Store.Filename,
			                            "mode",   Store.Mode);
			column.Clickable = true;
			column.SortOrder = SortType.Ascending;
			column.Clicked  += new EventHandler(this.OnFilenameColumnClicked);
			column.SetCellDataFunc(text, new TreeCellDataFunc(this.RenderEntry));
			this.tv.InsertColumn(column, (int)Store.Filename);
			column.SortIndicator = true;
			column.Resizable = true;
			
			// serial column
			text = new CellRendererText();
			text.Xalign = 0f;
			this.columns.Add(text);
			column = new TreeViewColumn(Catalog.GetString("Serial number"), text,
			                            "markup", Store.Serial,
			                            "mode",   Store.Mode);
			column.Clickable = true;
			column.SortOrder = SortType.Ascending;
			column.Clicked  += new EventHandler(this.OnSerialColumnClicked);
			column.SetCellDataFunc(text, new TreeCellDataFunc(this.RenderEntry));
			this.tv.InsertColumn(column, (int)Store.Serial);
						
			tv.Model = this.filter;
			tv.HeadersClickable = true;
			tv.HeadersVisible   = true;

			this.PackStart(hbox, false, false, 0);
			
			ScrolledWindow sw = new ScrolledWindow();
			sw.Add(this.tv);
			this.PackStart(sw, true, true, 0);
			
			this.InitializeList(path);
			
			this.tv.DoubleClicked  += new FileTv.DoubleClickedHandler(this.OnDoubleClicked);
			this.tv.ReturnPressed  += new FileTv.ReturnPressedHandler(this.OnDoubleClicked);
			this.tv.DeletePressed  += new FileTv.DeletePressedHandler(this.OnDeletePressed);
			this.tv.Button1Clicked += new FileTv.Button1ClickedHandler(this.OnButton1Clicked);
			this.tv.Button3Clicked += new FileTv.Button3ClickedHandler(this.OnButton3Clicked);
			
			this.ShowAll();
			
			this.ProcessFsWatcherQueue();
		}
		
		private bool FilterTree(TreeModel model, TreeIter iter)
		{
			if (this.filter_entry.Regex == null)
				return true;
			
			CellRendererMode mode = (CellRendererMode)model.GetValue(iter, (int)Store.Mode);
			if (mode == CellRendererMode.Inert)
				return false;
			
			string fname  = (string)model.GetValue(iter, (int)Store.Filename);
			string serial = (string)model.GetValue(iter, (int)Store.Serial);
			if (string.IsNullOrEmpty(fname) || 
			    string.IsNullOrEmpty(serial))
				return true;
			
			return this.filter_entry.Regex.IsMatch(fname) ||
				   this.filter_entry.Regex.IsMatch(fname);
		}

		private bool ProcessFsWatcherQueue()
		{
			try
			{
				int count = 0;
				bool updated = false;
				while ((this.FsEventQueue.Count > 0) && (count++ < 5) && this.__close_request != true)
				{
					FsWatcherEvent e = (FsWatcherEvent)this.FsEventQueue.Dequeue();
					FileInfo file;
					TreeIter parent, iter;
					updated = true;
					
					try
					{
						switch (e.Type)
						{
							
						case FsWatcherEventType.Changed:
							file   = new FileInfo(e.FullPath);
							iter   = this.GetIterByFileName(e.FullPath);
							parent = this.GetIterByDirName(file.DirectoryName);
							this.store.Remove(ref iter);
							if (e.IsSpectrum)
								this.AddSpectrumFile(parent, e.Spectrum);
							else
								this.AddUnknownFile(parent, file);
							this.store.ChangeSortColumn();
							FileTv.Cache.Remove(e.FullPath);
							break;
							
						case FsWatcherEventType.Created:
							file   = new FileInfo(e.FullPath);
							parent = this.GetIterByDirName(file.DirectoryName);
							if (e.IsSpectrum)
								this.AddSpectrumFile(parent, e.Spectrum);
							else
								this.AddUnknownFile(parent, file);
							this.store.ChangeSortColumn();
							break;
							
						case FsWatcherEventType.Deleted:
							iter = this.GetIterByFileName(e.FullPath);
							FileTv.Cache.Remove(e.FullPath);
							this.store.Remove(ref iter);
							FileTv.Cache.Remove(e.FullPath);
							break;
							
						default:
							throw new Exception(Catalog.GetString("Unknown file system watcher event:") +
							                    " " + Enum.GetName(typeof(FsWatcherEventType), e.Type));
							
						}
					}
					catch (Exception ex)
					{
						Console.WriteLine("Exception in ProcessFsEventQueue(): " + ex);
					}
				}
			
				// make sure events are processed once in a while
				if (updated)
					UserInterface.ProcessEvents_(Thread.CurrentThread.GetHashCode());
				
				if (count > 0)
					GLib.Timeout.Add(10, this.ProcessFsWatcherQueue);
				else
					GLib.Timeout.Add(500, this.ProcessFsWatcherQueue);
				
				return false;
			}
			catch (Exception ex)
			{
				throw new Exception(Catalog.GetString("Unhandled exception in ProcessFsWatcherQueue():") +
				                    " " + ex.Message);
			}
		}
		
		public void Close()
		{
			this.__close_request = true;

			List<string> paths = UserInterface.Settings.Current.DataBrowserPaths;
			paths.Remove(this.directory_path);
			UserInterface.Settings.Current.DataBrowserPaths = paths;

			this.tv.Selection.UnselectAll();
			
			this.fs_watcher.Dispose();
			this.Destroy();

			if (this.Closed != null)
				this.Closed();
		}
		
		private void ExploreDirectory(DirectoryInfo dir_info, TreeIter parent)
		{
			DirectoryInfo[] dirs;
			try
			{
				dirs = dir_info.GetDirectories();
			}
			catch (Exception ex)
			{
				new InfoMessage(null, 
				                Catalog.GetString("Cannot get subdirectories in directory '{0}': {1}"), 
				                dir_info.Name, ex.Message);
				return;
			}
			
			foreach (DirectoryInfo dir in dirs)
			{
				DirectoryProperty dp = new DirectoryProperty(dir);
				this.Closed += new ClosedHandler(dp.DisconnectDevice);
				
				TreeIter iter;
				iter = this.store.AppendValues(parent,
				                               "<b>" + dir.Name + "</b>",
				                               "",
				                               CellRendererMode.Activatable,
				                               dir.FullName,
				                               false,
				                               dp);
				
				Console.WriteLine("Add directory: " + dir.FullName);

				this.ExploreDirectory(dir, iter);
			}

			FileInfo[] files;
			try
			{
				files = dir_info.GetFiles(this.file_mask);
			}
			catch (Exception ex)
			{
				// Windows emmits an error if no file according the file mask is found
				if (!ex.Message.Contains("ERROR_NO_MORE_FILES"))
					new InfoMessage(null, 
					                Catalog.GetString("Cannot get files in directory '{0}': {1}"),
					                dir_info.Name, ex.Message);
				return;
			}
			
			Thread do_read_files_in_dir = new Thread(new ParameterizedThreadStart(this.DoReadFilesInDir));
			do_read_files_in_dir.IsBackground = true;
			UserInterface.Threads.Add(do_read_files_in_dir);
			do_read_files_in_dir.Start(files);
		}

		private void DoReadFilesInDir(object arg)
		{
			FileInfo[] files = arg as FileInfo[];

			foreach (FileInfo file in files)
			{
				if (this.__close_request)
					return;

				FileTv.Cache.Remove(file.FullName);
				
				try
				{
					try
					{
						Spectrum spectrum = CSV.Read(file.FullName, 25);
						this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Created, spectrum));
					}
					catch (System.IO.IOException)
					{
						// could be shared violation -> try once more after some time 
						Random rand = new Random(Thread.CurrentThread.GetHashCode());
						Thread.Sleep(rand.Next(50, 500));
						Spectrum spectrum = CSV.Read(file.FullName, 25);
						this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Created, spectrum));
					}
				}
				catch (Exception)
				{
					this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Created, file.FullName));
				}
			}

			string status = string.Format(Catalog.GetString("Finished reading directory with {0} files"), files.Length);
			UserInterface.Queues.StatusQueue.Enqueue(new Queues.StatusEvent("file_browser", status));
		}
		
		private void AddSpectrumFile(TreeIter parent, Spectrum spectrum)
		{
			if (spectrum.IsComplete)
				FileTv.Cache.Add(spectrum);
			
			this.store.AppendValues(parent,
			                        spectrum.FileInfo.Name,
			                        spectrum.Serial,
			                        CellRendererMode.Activatable,
			                        spectrum.FileInfo.FullName,
			                        true,
			                        null);		
			this.tv.ExpandRow(this.store.GetPath(parent), false);
			
			if (!this.open_all_button.Sensitive)
				this.open_all_button.Sensitive = true;
		}
		
		private void AddUnknownFile(TreeIter parent, FileInfo file)
		{
			this.store.AppendValues(parent,
			                        file.Name,
			                        string.Empty,
			                        CellRendererMode.Inert,
			                        file.FullName,
			                        false,
			                        null);
			this.tv.ExpandRow(this.store.GetPath(parent), false);
		}
		
		private void RenderEntry(TreeViewColumn column, CellRenderer cell, TreeModel model, TreeIter iter)
		{
			CellRendererMode mode = (CellRendererMode)model.GetValue(iter, (int)Store.Mode);

			if (mode == CellRendererMode.Inert)
				(cell as CellRendererText).Sensitive = false;
			else
				(cell as CellRendererText).Sensitive = true;
		}
		
		public void SelectParentDir()
		{
			TreeIter iter;
			if (this.store.GetIterFirst(out iter))
			{
				this.tv.Selection.SelectIter(iter);
			}
		}
		
		internal void InitializeList(string path)
		{
			DirectoryInfo dir_info = new DirectoryInfo(path);
			
			this.store.Clear();
			
			DirectoryProperty dp = new DirectoryProperty(dir_info);
			this.Closed += new ClosedHandler(dp.DisconnectDevice);
			
			TreeIter iter = this.store.AppendValues("<b>" + dir_info.Name + "</b>",
			                                        "",
			                                        CellRendererMode.Activatable,
			                                        dir_info.FullName,
			                                        false,
			                                        dp);
			this.ExploreDirectory(dir_info, iter);
				
			this.fs_watcher = new FileSystemWatcher(path, this.file_mask);
			this.fs_watcher.IncludeSubdirectories = true;
			this.fs_watcher.Changed += new FileSystemEventHandler(this.OnFsChanged);
			this.fs_watcher.Created += new FileSystemEventHandler(this.OnFsCreated);
			this.fs_watcher.Deleted += new FileSystemEventHandler(this.OnFsDeleted);
			this.fs_watcher.Renamed += new RenamedEventHandler(this.OnFsRenamed);
			this.fs_watcher.EnableRaisingEvents = true;
			
			this.store.SetSortFunc(0, new TreeIterCompareFunc(this.CompareFilenames));
			this.store.SetSortFunc(1, new TreeIterCompareFunc(this.CompareSerials));
			
			this.store.SetSortColumnId(0, SortType.Ascending);
			
			this.tv.ExpandRow(this.store.GetPath(iter), false);
		}
		
		private int CompareFilenames(TreeModel model, TreeIter a, TreeIter b)
		{
			return String.Compare((string)model.GetValue(a, (int)Store.Filename),
			                      (string)model.GetValue(b, (int)Store.Filename));
		}
		
		private int CompareSerials(TreeModel model, TreeIter a, TreeIter b)
		{
			return String.Compare((string)model.GetValue(a, (int)Store.Serial),
			                      (string)model.GetValue(b, (int)Store.Serial));
		}

		private List<DirectoryProperty> SelectedDirs(TreeSelection selection)
		{
			TreeModel model;
			TreeIter iter;
			
			TreePath[] paths = selection.GetSelectedRows(out model);
			List<DirectoryProperty> selected_dirs = new List<DirectoryProperty>();

			foreach (TreePath path in paths)
			{
				if (this.store.GetIterFromString(out iter, path.ToString()))
				{
					DirectoryProperty dir = 
						(DirectoryProperty)this.store.GetValue(iter, (int)Store.DirectoryProperty);

					if (dir == null)
						continue;

					Console.WriteLine("Selected: " + dir.Info.FullName);
					selected_dirs.Add(dir);
				}
			}
				
			return selected_dirs;
		}

		internal List<Spectrum> GetSelectedSpectra(TreeSelection selection)
		{
			TreeModel model;
			TreeIter iter;
			
			TreePath[] paths = selection.GetSelectedRows(out model);
			List<Spectrum> selected_spectra = new List<Spectrum>();

			foreach (TreePath path in paths)
			{
				if (this.tv.Model.GetIterFromString(out iter, path.ToString()) &&
				    (bool)this.tv.Model.GetValue(iter, (int)Store.IsSpectrum))
				{
					string fname = (string)this.tv.Model.GetValue(iter, (int)Store.Path);

					Spectrum spectrum;
					try
					{
						spectrum = FileTv.Cache.Get(fname);
					}
					catch { continue; }
					
					Console.WriteLine("Selected: " + spectrum.Id);
					selected_spectra.Add(spectrum);
				}
			}
			
			return selected_spectra;
		}
		
		internal List<Spectrum> GetAllSpectra()
		{
			List<Spectrum> list = new List<Spectrum>();
			
			this.tv.Model.Foreach(delegate(TreeModel model, TreePath path, TreeIter iter) {
				string fname = (string)model.GetValue(iter, (int)Store.Path);
				Spectrum spectrum;
				try
				{
					spectrum = FileTv.Cache.Get(fname);
				}
				catch
				{
					return false; 
				}
				
				list.Add(spectrum);
				return false;
			});
			
			return list;
		}
		
//		private List<TreePath> GetPaths()
//		{
//			lock (this)
//			{
//				this._paths.Clear();
//				this.store.Foreach(new TreeModelForeachFunc(this.__GetPathsCallback));
//				
//				return this._paths;
//			}
//		}
//
//		private bool __GetPathsCallback(TreeModel model, TreePath path, TreeIter iter)
//		{
//			this._paths.Add(model.GetPath(iter));
//			return false;
//		}
			             
		private List<TreeIter> GetIters()
		{
			this._iters.Clear();
			this.store.Foreach(new TreeModelForeachFunc(this.GetIterCallback));
			
			return this._iters;
		}

		private bool GetIterCallback(TreeModel model, TreePath path, TreeIter iter)
		{
			this._iters.Add(iter);
			return false;
		}
		
		private TreeIter GetIterByFileName(string full_name)
		{
			lock (this)
			{
				foreach (TreeIter iter in this.GetIters())
				{
					string name = (string)this.store.GetValue(iter, (int)Store.Path);
					
					if (name == null)
						continue;
					
					if (name == full_name)
						return iter;
				}
			}
			
			throw new Exception(Catalog.GetString("Tree store does not contain the file") +
			                    " '" + full_name + "'");
		}
		
		public List<DirectoryProperty> GetDirectoryProperties()
		{
			List<DirectoryProperty> list = new List<DirectoryProperty>();
			
			lock (this)
			{
				foreach (TreeIter iter in this.GetIters())
				{
					DirectoryProperty dp = (DirectoryProperty)this.store.GetValue(iter, (int)Store.DirectoryProperty);
					
					if (dp == null)
						continue;

					list.Add(dp);
				}
			}
			
			return list;
		}
		
		public bool SelectDirectory(DirectoryProperty dir_property)
		{
			lock (this)
			{
				foreach (TreeIter iter in this.GetIters())
				{
					DirectoryProperty dp = (DirectoryProperty)this.store.GetValue(iter, (int)Store.DirectoryProperty);
					
					if (dp == null)
						continue;

					if (dp == dir_property)
					{
						this.tv.Selection.SelectIter(iter);
						return true;
					}
				}
			}
			
			return false;
		}
		
		private TreeIter GetIterByDirName(string full_name)
		{
			lock (this)
			{
				foreach (TreeIter iter in this.GetIters())
				{
					DirectoryProperty dp = (DirectoryProperty)this.store.GetValue(iter, (int)Store.DirectoryProperty);
					
					if (dp == null)
						continue;
					
					if (dp.Info.FullName == full_name)
						return iter;
				}
			}
			
			throw new Exception(Catalog.GetString("Tree store does not contain the directory") +
			                    " '" + full_name + "'");
		}
		
		public SortType SetSortOrder(TreeViewColumn column)
		{
			if (column.SortIndicator)
			{
				if (column.SortOrder == SortType.Ascending)
					return SortType.Descending;
				else
					return SortType.Ascending;
			}
			return SortType.Ascending;
		}

		private void OnFilenameColumnClicked(object o, EventArgs e)
		{
			TreeViewColumn c = o as TreeViewColumn;
			c.SortOrder      = this.SetSortOrder(c);
			c.SortIndicator  = true;
			
			lock (this)
				this.store.SetSortColumnId(0, c.SortOrder);
		}
		
		private void OnSerialColumnClicked(object o, EventArgs e)
		{
			TreeViewColumn c = o as TreeViewColumn;
			c.SortOrder      = this.SetSortOrder(c);
			c.SortIndicator  = true;
			
			lock (this)
				this.store.SetSortColumnId(1, c.SortOrder);
		}

		private void OnMotion(object o, Gdk.EventMotion e)
		{
			try
			{
				TreePath path;
				TreeIter iter;
				
				this.tv.GetPathAtPos((int)e.X, (int)e.Y, out path);

				lock (this)
					this.store.GetIterFromString(out iter, path.ToString());
				
				if ((this.current_tooltip_path != null) && 
				    (this.current_tooltip_path.ToString() == path.ToString()) &&
				    FileTv.Tooltip != null)
				{
					FileTv.Tooltip.Move((int)e.XRoot + 12,
					                    (int)e.YRoot + 12);
					return;
				}
				
				this.current_tooltip_path = path;
				
				if (FileTv.Tooltip != null)
				{
					FileTv.Tooltip.Destroy();
					FileTv.Tooltip = null;
				}
				
				if (!(bool)this.store.GetValue(iter, (int)Store.IsSpectrum))
					return;
				
				string fname = (string)this.store.GetValue(iter, (int)Store.Path);
				
				Spectrum spectrum;
				try
				{
					spectrum = FileTv.Cache.Get(fname);
				}
				catch { return; }

				FileTv.Tooltip = new SpectrumTooltip(spectrum, (int)e.XRoot + 12, (int)e.YRoot + 12);
			}
			catch
			{
				if (FileTv.Tooltip != null)
				{
					FileTv.Tooltip.Destroy();
					FileTv.Tooltip = null;
				}
				
				this.current_tooltip_path = null;
			}
		}
		
		private void OnLeft(object o, Gdk.EventCrossing e)
		{
			if (FileTv.Tooltip == null)
				return;
			
			FileTv.Tooltip.Destroy();
			FileTv.Tooltip = null;
			this.current_tooltip_path = null;
		}
		
		private void OnDeletePressed(FileTv tv)
		{
			List<Spectrum> spectra = this.GetSelectedSpectra(tv.Selection);		

			try
			{
				foreach (Spectrum spectrum in spectra)
				{
					FileInfo file = new FileInfo(spectrum.Id.Remove(0,12));
					Console.WriteLine("Delete spectrum file: " + file.FullName);

					DeleteDialog dialog = new DeleteDialog(null, file.Name);
					ResponseType result = (ResponseType)dialog.Run();
					dialog.Destroy();
					
					if (result == ResponseType.Yes)
						file.Delete();
				}
			}
			catch (Exception ex)
			{
				new InfoMessage(null, Catalog.GetString("Cannot delete spectrum file:") +
				                " " + ex.Message);
			}
		}

		private void OnDoubleClicked(FileTv tv)
		{
			List<Spectrum> spectra = this.GetSelectedSpectra(tv.Selection);		

			if ((spectra.Count == 1) && (this.SpectrumDoubleClicked != null))
			{
				this.SpectrumDoubleClicked(spectra[0].Clone());
				return;
			}
			
			List<DirectoryProperty> dirs = this.SelectedDirs(tv.Selection);
			
			if ((dirs.Count == 1) && (this.DirectoryDoubleClicked != null))
			{
				this.DirectoryDoubleClicked(dirs[0]);
				
				return;
			}
		}
		
		private void OnButton1Clicked(FileTv tv)
		{
			if (this.DirectorySelectionChanged != null)
				this.DirectorySelectionChanged(this.SelectedDirs(tv.Selection));
			
			if (this.SpectrumSelectionChanged != null)
				this.SpectrumSelectionChanged(this.GetSelectedSpectra(tv.Selection));
		}

		private void OnButton3Clicked(FileTv tv)
		{
			Console.WriteLine("Right Click");
		}
				
		private void OnSelectionChanged(object o, System.EventArgs e)
		{
			//TreeSelection selection = o as TreeSelection;
			TreeSelection selection = this.tv.Selection;
			
			if (this.DirectorySelectionChanged != null)
				this.DirectorySelectionChanged(this.SelectedDirs(selection));

			if (this.SpectrumSelectionChanged != null)
				this.SpectrumSelectionChanged(this.GetSelectedSpectra(selection));
		}
		
		private void OnFsChanged(object o, FileSystemEventArgs e)
		{
			Console.WriteLine("A file has been changed: " + e.Name);
			Thread.Sleep(100);
			
			try
			{
				try
				{
					Spectrum spectrum = CSV.Read(e.FullPath, 25);
					this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Changed, spectrum));
				}
				catch (System.IO.IOException)
				{
					// could be shared violation -> try once more after some time 
					Random rand = new Random(Thread.CurrentThread.GetHashCode());
					Thread.Sleep(rand.Next(100, 1000));
					Spectrum spectrum = CSV.Read(e.FullPath, 25);
					this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Changed, spectrum));
				}
			}
			catch (Exception)
			{
				this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Changed, e.FullPath));
			}
		}
		
		private void OnFsCreated(object o, FileSystemEventArgs e)
		{
			Console.WriteLine("A file has been created: " + e.Name);
			Thread.Sleep(100);

			try
			{
				Spectrum spectrum = CSV.Read(e.FullPath, 25);
				this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Created, spectrum));
			}
			catch (Exception)
			{
				this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Created, e.FullPath));
			}
		}
		
		private void OnFsDeleted(object o, FileSystemEventArgs e)
		{
			Console.WriteLine("A file has been deleted: " + e.Name);

			this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Deleted, e.FullPath));
		}
		
		private void OnFsRenamed(object o, RenamedEventArgs e)
		{
			Console.WriteLine("A file has been renamed: " + e.OldName + " -> " + e.Name);

			this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Deleted, e.OldFullPath));
			
			try
			{
				Spectrum spectrum = CSV.Read(e.FullPath, 25);
				this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Created, spectrum));
			}
			catch (Exception)
			{
				this.FsEventQueue.Enqueue(new FsWatcherEvent(FsWatcherEventType.Created, e.FullPath));
			}
		}

		public TreeView Tv
		{
			get { return this.tv; }
		}
		
		private Queue FsEventQueue
		{
			get { return this.fs_event_queue; }
		}
	}
	
	public class NotebookLabel : Gtk.Alignment // Gtk.EventBox
	{
		private HBox   hbox;
		private Label  label;
		private Button button;

		public delegate void CloseClickedHandler();
		public event         CloseClickedHandler CloseClicked;
		
		public delegate void Button3ClickedHandler();
		public event         Button3ClickedHandler Button3Clicked;
		
		public NotebookLabel(string text, string tooltip) : base(0f, 0f, 1f, 1f) // base()
		{
			if (text.Length > 25)
				text = text.Remove(21) + "...";

			this.label = new Label(text);
			this.label.TooltipText  = tooltip;
			this.label.UseUnderline = false;

			this.button = new CloseButton();
			this.button.Clicked += new EventHandler(OnButtonClicked);

			this.hbox = new HBox();
			this.hbox.PackStart(this.button, false, false, 0);
			this.hbox.PackStart(this.label,  false, false, 2);

			this.Name = this.hbox.Name;
			
			
			// FIXME: workaround for bug 664 (widgets in Eventbox not visible)
//			if (MainClass.OS == MainClass.OSType.Windows)
//			{
//				this.Add(hbox);
//			}
//			else
			{
				EventBox eb = new EventBox();
				eb.ButtonPressEvent += delegate(object o, ButtonPressEventArgs args) {
					if (args.Event.Button == 2 && this.CloseClicked != null)
						this.CloseClicked();
					else if (args.Event.Button == 3 && this.Button3Clicked != null)
						this.Button3Clicked();
				};
				eb.Add(hbox);
				this.Add(eb);
			}
			
			this.ShowAll();
		}
		
//		protected override bool OnButtonPressEvent(Gdk.EventButton evnt)
//		{
//			if (evnt.Button == 2 && this.CloseClicked != null)
//				this.CloseClicked();
//			return base.OnButtonPressEvent (evnt);
//		}
		
		private void OnButtonClicked(object o, System.EventArgs e)
		{
			if (this.CloseClicked != null)
				this.CloseClicked();
		}
		
		public string Label
		{
			get { return this.label.Text;  }
			set { this.label.Text = value; }
		}
		
		public bool ButtonVisible
		{
			get { return this.button.Visible;  }
			set
			{
				this.button.Visible = value;
				this.button.NoShowAll = !value;
			}
		}
	}
	
	internal class TooltipLabel : Gtk.Label
	{
		public TooltipLabel(string str) : base(str)
		{
		}
		
		protected override bool OnEnterNotifyEvent (Gdk.EventCrossing evnt)
		{
			return base.OnEnterNotifyEvent (evnt);
		}
	}
		
	
    public class SpectrumTooltip : Hiperscan.GtkExtensions.Widgets.RoundedWindow
	{
		private readonly double timeout = 7f;
		
		public SpectrumTooltip(Spectrum spectrum, int x, int y) : base(10)
		{
			Table table = new Table(4, 4, false);
			
			Alignment align = new Alignment(0f, 0.5f, 0f, 1f);
//			align.Add(new TooltipLabel(Catalog.GetString("Device:")));
//			table.Attach(align, 0, 1, 0, 1, AttachOptions.Fill, AttachOptions.Shrink, 0, 0);
			
			align = new Alignment(0f, 0.5f, 0f, 1f);
			string s = spectrum.HasAbsorbance ? Catalog.GetString("Absorbance") : Catalog.GetString("Intensity");
			align.Add(new TooltipLabel(s));
			table.Attach(align, 1, 2, 0, 1, AttachOptions.Fill, AttachOptions.Shrink, 3, 0);

//			align = new Alignment(0f, 0.5f, 0f, 1f);
//			align.Add(new TooltipLabel(Catalog.GetString("Absorbance:")));
//			table.Attach(align, 0, 1, 1, 2, AttachOptions.Fill, AttachOptions.Shrink, 0, 0);
			
			align = new Alignment(0f, 0.5f, 0f, 1f);
			align.Add(new TooltipLabel(spectrum.Serial));
			table.Attach(align, 1, 2, 1, 2, AttachOptions.Fill, AttachOptions.Shrink, 3, 0);

//			align = new Alignment(0f, 0.5f, 0f, 1f);
//			align.Add(new TooltipLabel(Catalog.GetString("Dark intensity:")));
//			table.Attach(align, 0, 1, 2, 3, AttachOptions.Fill, AttachOptions.Shrink, 0, 0);
			
			align = new Alignment(0f, 0.5f, 0f, 1f);
			align.Add(new TooltipLabel(spectrum.Timestamp.Date.ToShortDateString()));
			table.Attach(align, 1, 2, 2, 3, AttachOptions.Fill, AttachOptions.Shrink, 3, 0);

//			align = new Alignment(0f, 0.5f, 0f, 1f);
//			align.Add(new TooltipLabel(Catalog.GetString("Date:")));
//			table.Attach(align, 0, 1, 3, 4, AttachOptions.Fill, AttachOptions.Shrink, 0, 0);
			
			align = new Alignment(0f, 0.5f, 0f, 1f);
			align.Add(new TooltipLabel(spectrum.Timestamp.TimeOfDay.ToString()));
			table.Attach(align, 1, 2, 3, 4, AttachOptions.Fill, AttachOptions.Shrink, 3, 0);
			
			table.RowSpacing = 1;
			
			int height = 4 * ((new Label()).SizeRequest().Height + (int)table.RowSpacing);

			IPlotEngine plot = new NPlotEngine(PlotEngineType.Preview);
			plot.GtkWidget.HeightRequest = height;
			plot.GtkWidget.WidthRequest  = (int)(height * 1.618);
			DataSet ds;
			if (spectrum.HasAbsorbance)
				ds = spectrum.Absorbance;
			else
				ds = spectrum.Intensity;
			if (UserInterface.Settings.Current.AxisType == AxisType.Wavecount)
			{
				plot.ReverseXAxes = UserInterface.Settings.Current.AxisType == AxisType.Wavecount;
				ds = new DataSet(ds);
				ds.Id = spectrum.Id + "_wavecount";
				UserInterface.ConvertXAxisToWavecount(ref ds);
			}
			plot.AddDataSet(ds);
			plot.DefaultLineWidth = 1f;
			plot.FitView();

			table.Attach(plot.GtkWidget, 2, 3, 0, 4, AttachOptions.Fill, AttachOptions.Fill, 0, 0);
			
			this.EnterNotifyEvent += delegate(object o, EnterNotifyEventArgs args) {
				align = new Alignment(0f, 0f, 0f, 0f);
				CloseButton b = new CloseButton();
				b.Clicked += delegate(object sender, EventArgs e) {
					this.Destroy();
				};
				align.Add(b);
				table.Attach(align, 3, 4, 0, 4, AttachOptions.Fill, AttachOptions.Fill, 0, 0);
				table.ShowAll();
				this.UpdateShape();
			};
			
			this.ButtonPressEvent += delegate(object o, ButtonPressEventArgs args) {
				this.Destroy();
			};

			this.Title = "Tooltip";
			this.Name = "gtk-tooltips";
			this.BorderWidth = 4;
			this.Add(table);

			this.Move(x, y);
			this.ShowAll();
			plot.ProcessChanges();
			
			this.UpdateShape();

			GLib.Timeout.Add((uint)(this.timeout*1000), delegate {
				this.Destroy();
				return false;
			});
		}
		
//		public new void Move(int x, int y)
//		{
//			this.then = DateTime.Now;
//			base.Move(x, y);
//		}
	}	
}
