// 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.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;

using Gtk;

using Hiperscan.NPlotEngine;
using Hiperscan.QuickStep.Dialog;
using Hiperscan.QuickStep.Figure;
using Hiperscan.QuickStep.IO;
using Hiperscan.Spectroscopy;
using Hiperscan.Spectroscopy.IO;
using Hiperscan.Unix;

namespace Hiperscan.QuickStep.Treeview
{

    public class SpectrumCache
    {
        private ConcurrentDictionary<string,Spectrum> spectra;
        readonly int max_count;
        
        public SpectrumCache(int max_count)
        {
            this.max_count = max_count;
            this.spectra   = new ConcurrentDictionary<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.TryRemove(this.spectra.Keys.First(), out Spectrum value);

                this.spectra.TryAdd(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) ?? false)
                this.spectra.TryRemove(fname, out Spectrum value);
        }
        
        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)
        {
            this.Motion?.Invoke(this, e);

            return base.OnMotionNotifyEvent(e);
        }
        
        protected override bool OnLeaveNotifyEvent(Gdk.EventCrossing e)
        {
            this.Left?.Invoke(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 += (o, args) => this.ModifyStyle();
        }
        
        private void ModifyStyle()
        {
            if (this.Style.XThickness == 0 && this.Style.YThickness == 0)
                return;
            RcStyle style = new RcStyle
            {
                Xthickness = 0,
                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 readonly 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; }

            public string FullPath
            {
                get
                {
                    if (this.Spectrum != null)
                        return this.Spectrum.FileInfo.FullName;
                    else
                        return this.full_path;
                }
            }
        }
        
        private class FilterEntry : Gtk.Entry
        {
            private Gdk.Color error_text_color;
            private Gdk.Color standard_text_color;
            private Gdk.Color error_base_color;
            private Gdk.Color standard_base_color;
            
            public delegate void RegexChangedHandler(Regex regex);
            public event RegexChangedHandler RegexChanged;
            
            
            public FilterEntry(Gdk.Color error_text_color, Gdk.Color error_base_color) : base()
            {
                this.error_text_color = error_text_color;
                this.error_base_color = error_base_color;
                this.Changed += (sender, e) => this.UpdateRegexp();
                this.standard_text_color = this.Style.Text(StateType.Normal);
                this.standard_base_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.ModifyText(StateType.Normal, this.standard_text_color);
                    this.ModifyBase(StateType.Normal, this.standard_base_color);
                    changed = true;
                }
                catch (ArgumentException)
                {
                    this.Regex = null;
                    this.ModifyText(StateType.Normal, this.error_text_color);
                    this.ModifyBase(StateType.Normal, this.standard_base_color);
                    if (was_null == false)
                        changed = true;
                }

                if (changed && this.RegexChanged != null)
                    this.RegexChanged(this.Regex);
            }

            public Regex Regex { get; private set; } = null;
        }

        private FilterEntry filter_entry;
        private FileTv            tv;
        private readonly TreeStore store;
        private FileSystemWatcher fs_watcher;
        
        private readonly string directory_path;
        private TreeModelFilter filter;
        private Button clear_filter_button;
        private readonly Button open_all_button;

        private volatile bool __close_request = false;
        
        private readonly string file_mask = "*.csv";
        
        private readonly ArrayList columns = new ArrayList();
        private readonly 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)
            {
                VisibleFunc = new TreeModelFilterVisibleFunc(this.FilterTree)
            };

            this.filter_entry = new FilterEntry(UserInterface.entry_error_text_color, UserInterface.entry_error_base_color)
            {
                WidthChars = 20,
                TooltipText = Catalog.GetString("Regular expression for filename or serial number")
            };
            this.filter_entry.RegexChanged += regex =>
            {
                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))
            {
                TooltipText = Catalog.GetString("Clear regular expression"),
                Relief = ReliefStyle.None,
                Sensitive = false
            };
            this.clear_filter_button.Clicked += (sender, e) => this.filter_entry.Text = string.Empty;
            Alignment align = new Alignment(0f, 0.5f, 0f, 0f)
            {
                this.clear_filter_button
            };
            hbox.PackStart(align, false, false, 0);

            this.open_all_button = new Button(new Image(Stock.Open, IconSize.Menu))
            {
                TooltipText = Catalog.GetString("Open all"),
                Relief = ReliefStyle.None,
                Sensitive = false
            };
            this.open_all_button.Clicked += (sender, e) => this.OpenAllClicked?.Invoke(this.GetAllSpectra());
            align = new Alignment(0f, 0.5f, 0f, 0f)
            {
                this.open_all_button
            };
            hbox.PackStart(align, true, true, 3);

            this.tv = new FileTv(store)
            {
                WidthRequest  = 210,
                HeightRequest = 120,
                RulesHint     = false
            };
            this.tv.Selection.Mode = SelectionMode.Single;
            
            this.tv.Motion            += this.OnMotion;
            this.tv.Left              += this.OnLeft;
            this.tv.Selection.Changed += this.OnSelectionChanged;
            
            CellRendererText text;
            TreeViewColumn   column;

            // filename column
            text = new CellRendererText
            {
                Xalign = 0f
            };
            this.columns.Add(text);
            column = new TreeViewColumn(Catalog.GetString("Spectrum file"), text,
                                        "markup", Store.Filename,
                                        "mode", Store.Mode)
            {
                Clickable = true,
                SortOrder = SortType.Ascending
            };
            column.Clicked  += 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
            {
                Xalign = 0f
            };
            this.columns.Add(text);
            column = new TreeViewColumn(Catalog.GetString("Serial number"), text,
                                        "markup", Store.Serial,
                                        "mode", Store.Mode)
            {
                Clickable = true,
                SortOrder = SortType.Ascending
            };
            column.Clicked  += 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
            {
                this.tv
            };
            this.PackStart(sw, true, true, 0);
            
            this.InitializeList(path);
            
            this.tv.DoubleClicked  += this.OnDoubleClicked;
            this.tv.ReturnPressed  += this.OnDoubleClicked;
            this.tv.DeletePressed  += this.OnDeletePressed;
            this.tv.Button1Clicked += this.OnButton1Clicked;
            this.tv.Button3Clicked += 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.Instance.ProcessEvents();
                
                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();

            this.Closed?.Invoke();
        }

        private void ExploreDirectory(DirectoryInfo dir_info, TreeIter parent)
        {
            DirectoryInfo[] dirs;
            try
            {
                dirs = dir_info.GetDirectories();
            }
            catch (Exception ex)
            {
                InfoMessageDialog.Show(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 += 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") == false)
                    InfoMessageDialog.Show(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))
            {
                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()
        {
            if (this.store.GetIterFirst(out TreeIter 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 += 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)
            {
                IncludeSubdirectories = true
            };
            this.fs_watcher.Changed += this.OnFsChanged;
            this.fs_watcher.Created += this.OnFsCreated;
            this.fs_watcher.Deleted += this.OnFsDeleted;
            this.fs_watcher.Renamed += 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)
        {
            TreePath[] paths = selection.GetSelectedRows(out TreeModel model);
            List<DirectoryProperty> selected_dirs = new List<DirectoryProperty>();

            foreach (TreePath path in paths)
            {
                if (this.store.GetIterFromString(out TreeIter 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)
        {
            TreePath[] paths = selection.GetSelectedRows(out TreeModel model);
            List<Spectrum> selected_spectra = new List<Spectrum>();

            foreach (TreePath path in paths)
            {
                if (this.tv.Model.GetIterFromString(out TreeIter 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
            {
                TreeIter iter;

                this.tv.GetPathAtPos((int)e.X, (int)e.Y, out TreePath 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) == false)
                    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)
            {
                InfoMessageDialog.Show(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)
        {
            this.DirectorySelectionChanged?.Invoke(this.SelectedDirs(tv.Selection));

            this.SpectrumSelectionChanged?.Invoke(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;

            this.DirectorySelectionChanged?.Invoke(this.SelectedDirs(selection));

            this.SpectrumSelectionChanged?.Invoke(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; } = Queue.Synchronized(new Queue());
    }

    public class NotebookLabel : Gtk.Alignment // Gtk.EventBox
    {
        private readonly HBox hbox;
        private readonly Label label;
        private readonly 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)
            {
                TooltipText = tooltip,
                UseUnderline = false
            };

            this.button = new CloseButton();
            this.button.Clicked += this.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 += (o, 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)
        {
            this.CloseClicked?.Invoke();
        }

        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 = 7.0;
        
        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)
            {
                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)
            {
                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)
            {
                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.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)
                {
                    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 += (o, args) =>
            {
                align = new Alignment(0f, 0f, 0f, 0f);
                CloseButton b = new CloseButton();
                b.Clicked += (sender, e) => this.Destroy();
                align.Add(b);
                table.Attach(align, 3, 4, 0, 4, AttachOptions.Fill, AttachOptions.Fill, 0, 0);
                table.ShowAll();
            };
            
            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();

            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);
//        }
    }    
}
