// UI.cs created with MonoDevelop
// User: klose at 13:33 27.12.2008
// CVS release: $Id: UI.cs,v 1.94 2011-06-24 13:48:23 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.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;

using Glade;

using Gtk;

using Hiperscan.Extensions;
using Hiperscan.QuickStep.Dialog;
using Hiperscan.QuickStep.Figure;
using Hiperscan.QuickStep.Instrument;
using Hiperscan.QuickStep.Integration;
using Hiperscan.QuickStep.IO;
using Hiperscan.QuickStep.Treeview;
using Hiperscan.SGS;
using Hiperscan.SGS.SerialInterface;
using Hiperscan.Spectroscopy;
using Hiperscan.Spectroscopy.IO;
using Hiperscan.Unix;


namespace Hiperscan.QuickStep
{

    public partial class UserInterface : Hiperscan.Extensions.IHost
    {
        private const string DEFAULT_SPECTRUM_FILENAME = "untitled_spectrum";

        [Widget] Gtk.VBox ui_vbox = null;
        
        [Widget] Gtk.MenuBar menubar = null;
        
        [Widget] Gtk.Frame data_sources_frame = null;
        [Widget] Gtk.Frame spectrometer_frame = null; 
        [Widget] Gtk.Frame plot_browser_frame = null; 
        [Widget] Gtk.Frame plot_tv_frame      = null; 

        [Widget] Gtk.HandleBox property_handlebox = null;

        [Widget] Gtk.Paned data_source_paned  = null;
        [Widget] Gtk.Paned spectrometer_paned = null;
        [Widget] Gtk.VBox  property_vbox = null;
                
        [Widget] Gtk.Statusbar statusbar  = null;
        [Widget] Gtk.Statusbar statusbar2 = null;

        [Widget] Gtk.ProgressBar progressbar = null;
        
        [Widget] Gtk.Notebook plot_notebook     = null;
        [Widget] Gtk.Notebook file_notebook     = null;
        [Widget] Gtk.Notebook property_notebook = null;

        
        [Widget] Gtk.Frame property_frame = null;
        [Widget] Gtk.Button attach_property_editor_button = null;
        
        [Widget] Gtk.CheckButton preview_checkbutton = null;
        [Widget] Gtk.CheckButton legend_checkbutton  = null;
        
        [Widget] Gtk.Label current_object_label = null;
        
        [Widget] Gtk.Menu help_menu = null;
        [Widget] Gtk.ImageMenuItem help_item = null;
        [Widget] Gtk.ImageMenuItem quit_item = null;
        [Widget] Gtk.SeparatorMenuItem help_separator_item = null;
        
        [Widget] Gtk.Alignment service_dialog_align   = null;
        [Widget] Gtk.Alignment extract_data_align     = null;
        [Widget] Gtk.Alignment browse_directory_align = null;
        
        [Widget] Gtk.Image add_plot_tab_image = null;
        
        private List<Widget> standard_toolbar_widgets  = new List<Widget>();
        private List<Widget> extension_toolbar_widgets = new List<Widget>();
        
        private Dictionary<string,FileFilter> file_filters = new Dictionary<string,FileFilter>();
        
        internal Glade.XML gui;

        private ScrolledSpectrometerTv spectrometer_tv; 
        private ScrolledPlotTv         plot_tv;
        
        private volatile bool initialized = false;
        private bool bg_message_is_pending = false;

        private Gdk.Pixbuf clipboard_pixbuf_data;
        private string clipboard_csv_data;
        
        public event EventHandler<EventArgs> ReplaceGui;
        public event EventHandler<EventArgs> Exit;
        public event EventHandler<EventArgs> UnhandledException;
        public event EventHandler<EventArgs> InfoMessageShown;
        
        
        public UserInterface(Window parent)
        {
            this.MainWindow = parent;
            UserInterface.Instance = this;
            this.GuiThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

            Console.WriteLine("Create GUI...");
            try
            {
                this.gui = new Glade.XML(null, "UserInterface.glade", "ui_vbox", MainClass.ApplicationDomain);                
            }
            catch(Exception ex)
            {
                throw new Exception($"Cannot create GUI: {ex.Message}", ex);
            }
            Console.WriteLine("Autoconnect signals...");
            gui.Autoconnect(this);
            this.MainWindow.AddAccelGroup(this.gui.EnsureAccel());
            LinkButton.SetUriHook(delegate(LinkButton l, string s) {});
            
            Console.WriteLine("Initialize components:");

            Console.WriteLine("\tSettings...");
            this.LoadSettings();
            
            Console.WriteLine("\tThemes...");
            this.InitializeThemes();
            Console.WriteLine("\tQueues...");
            this.InitializeQueues();
            Console.WriteLine("\tMainWindow...");
            this.InitializeMainWindow();
            Console.WriteLine("\tSpectrometerTv...");
            this.InitializeSpectrometerTv();
            Console.WriteLine("\tPlotTv...");
            this.InitializePlotTv();
            Console.WriteLine("\tFigure...");
            this.InitializeFigure();
            Console.WriteLine("\tLinkButtons...");
            this.InitializeLinkButtons();
            Console.WriteLine("\tFileTv...");
            this.InitializeFileTv();
            Console.WriteLine("\tPropertiesSpectrometer...");
            this.InitializePropertiesSpectrometer();
            Console.WriteLine("\tPropertiesDirectory...");
            this.InitializePropertiesDirectory();
            Console.WriteLine("\tPropertiesSpectrum...");
            this.InitializePropertiesSpectrum();
            Console.WriteLine("\tTools...");
            this.InitializeTools();
            Console.WriteLine("\tFileTypes...");
            this.InitializeFileTypes();
            Console.WriteLine("\tMenu...");
            this.InitializeMenu();
            Console.WriteLine("\tToolbar...");
            this.InitializeToolbar();
            Console.WriteLine("\tStatusbar...");
            this.InitializeStatusBar();
            Console.WriteLine("\tPlugins...");
            this.InitializePlugins();
            Console.WriteLine("\tAcceleration editor...");
            this.InitializeAccelerationEditor();
            Console.WriteLine("\tReleaseVersion...");
            this.InitializeReleaseVersion();
            Console.WriteLine("\tSet device list events...");
            this.InitializeDeviceListEvents();
            Console.WriteLine("Init done.");
            this.initialized = true;
        }
        
        public static Gtk.Widget NewLabelButton(Glade.XML xml, GLib.GType widget_type, Glade.WidgetInfo info)
        {
            return new LinkButton(info.properties.Value);
        }
        
        public bool DoPostInitTasks()
        {
            UserInterface.Queues.StartProcessing();
            
            if (MainClass.AutoSharePorts.Count > 0)
            {
                GLib.Timeout.Add(15000, () =>
                {
                    foreach (uint port in MainClass.AutoSharePorts)
                    {
                        foreach (IDevice dev in this.DeviceList.Values)
                        {
                            if (dev.IsIdle == false)
                                continue;

                            this.ShareDevice(this.MainWindow, dev, port);
                            break;
                        }
                    }
                    Integration.TrayIcon.SetTooltip(RemoteSerialInterface.SharedCount,
                                                    RemoteSerialInterface.ConnectedCount);
                    return false;
                });
            }

            this.ReplaceGui?.Invoke(null, EventArgs.Empty);

            StatusMessageWindow.EnableMessages = true;


            //TODO
            /*
            if (Environment.GetEnvironmentVariable(Common.Env.QuickStep.NO_DEVICE_HOTPLUG) != "1")
            {
                Thread t = new Thread(new ThreadStart(() => this.DeviceList.StartAutoUpdate()));
                t.Start();
            }
            else
            {
                Thread t = new Thread(new ThreadStart(() => this.DeviceList.Update()));
                t.Start();
            }*/

            bool changed = false;
            int plot_tab_page = this.plot_notebook.CurrentPage;
            
            foreach (string fname in MainClass.AutoOpenFiles)
            {
                try
                {
                    Spectrum[] spectra;
                    List<MetaData> meta_data = null;
                    
                    if (Path.GetExtension(fname).ToLower() == ".csv")
                        spectra = new Spectrum[] { CSV.Read(fname) };
                    else if (Path.GetExtension(fname).ToLower() == ".csvc")
                        spectra = CSV.ReadContainer(fname, out meta_data);
                    else
                        throw new ArgumentException(Catalog.GetString("Not supported file type."));

                    this.plot_notebook.CurrentPage = plot_tab_page;

                    foreach (Spectrum spectrum in spectra)
                    {
                        this.AddSpectrum(spectrum);
                    }
                    
                    this.ScrolledPlot.RestoreAnnotations(meta_data);
                    changed = true;
                }
                catch (Exception ex)
                {
                    InfoMessageDialog.Show(this.MainWindow, 
                                           Catalog.GetString("Cannot open spectrum file: {0}"), 
                                           ex.Message);
                }
            }
            if (changed)
                this.ScrolledPlot.Plot.RequestUpdate();

            return false;
        }
        

        public void NotifyExit()
        {
            this.Exit?.Invoke(this, EventArgs.Empty);
        }
        
        public void NotifyUnhandledException()
        {
            this.UnhandledException?.Invoke(this, EventArgs.Empty);
        }
        
        private void InitializeReleaseVersion()
        {
            if (MainClass.IsRelease == false)
                return;
            
            this.help_menu.Remove(this.help_item);
            this.help_menu.Remove(this.help_separator_item);
        }
            
        private void InitializeMainWindow()
        {
            if (Settings.Current.MainWindowWidth > 0)
                this.MainWindow.DefaultWidth = Settings.Current.MainWindowWidth;
            else
                this.MainWindow.DefaultWidth = 1100;
            
            if (Settings.Current.MainWindowHeight > 0)
                this.MainWindow.DefaultHeight = Settings.Current.MainWindowHeight;
            else
                this.MainWindow.DefaultHeight = 700;
            
            if (Settings.Current.MainWindowMaximized)
                this.MainWindow.Maximize();
            
            if (Settings.Current.SpectrometerPanedPosition > 0)
                this.spectrometer_paned.Position = Settings.Current.SpectrometerPanedPosition;
            
            if (Settings.Current.DataSourcePanedPosition > 0)
                this.data_source_paned.Position = Settings.Current.DataSourcePanedPosition;
            
            Drag.DestSet(this.MainWindow, DestDefaults.All, 
                         UserInterface.TargetEntries, 
                         Gdk.DragAction.Copy);

            this.MainWindow.DragDataReceived += this.OnDragDataReceived;

            Key.SnooperInstall(this.OnKeyPressed);
        }

        private void InitializeQueues()
        {
            UserInterface.Queues = new Queues(this)
            {
                Statusbar = this.statusbar
            };
            UserInterface.Threads = new List<Thread>();
            
            UserInterface.Queues.InfoMessageShown += (sender, e) => this.InfoMessageShown?.Invoke(sender, e);
        }
        
        private void InitializeTools()
        {
            this.StandardToolbarVisible   = Settings.Current.StandardToolbarVisible;
            this.FigureToolbarVisible     = Settings.Current.FigureToolbarVisible;
            this.ExtensionsToolbarVisible = Settings.Current.ExtensionsToolbarVisible;
            this.DataSourcesVisible       = Settings.Current.DataSourcesVisible;
            this.PropertyEditorVisible    = Settings.Current.PropertyEditorVisible;
            this.PlotBrowserVisible       = Settings.Current.PlotBrowserVisible;
            
            ListStore store = new ListStore(typeof(Gdk.Pixbuf), typeof(LineStyle));
            CellRendererPixbuf pixbuf = new CellRendererPixbuf();
            this.plot_style_combo.Clear();
            this.plot_style_combo.PackStart(pixbuf, false);
            this.plot_style_combo.SetAttributes(pixbuf, "pixbuf", 0);
            foreach (LineStyle style in Enum.GetValues(typeof(LineStyle)))
            {
                store.AppendValues(this.ScrolledPlot.Plot.GetLineStylePixbuf(style), style);
            }
            this.plot_style_combo.Model = store;
        }
        
        private void InitializeLinkButtons()
        {
            this.service_dialog_button = new LinkButton(Catalog.GetString("Service dialogue"));
            this.service_dialog_button.Clicked += this.OnServiceDialogButtonClicked;
            this.service_dialog_align.Add(this.service_dialog_button);
            
            LinkButton b = new LinkButton(Catalog.GetString("Extract data"));
            b.Clicked += this.OnAdditionalDataClicked;
            this.extract_data_align.Add(b);
            
            b = new LinkButton(Catalog.GetString("Open directory"));
            b.Clicked += this.OnBrowseDirClicked;
            this.browse_directory_align.Add(b);
        }
        
        private void InitializeStatusBar()
        {
            UserInterface.Queues.StatusQueue.Enqueue(new Queues.StatusEvent("gui", Catalog.GetString("Ready")));
            StatusMessageWindow.XOffset = 10;
            StatusMessageWindow.YOffset = this.statusbar.SizeRequest().Height;
            this.progressbar.Visible = false;
            this.progressbar.NoShowAll = true;
        }
        
        private void InitializeSpectrometerTv()
        {
            if (DeviceList.IsInitialized == false)
                DeviceList.Init(MainClass.UsbConfigs);

            this.DeviceList = DeviceList.Get();
            this.DeviceList.DisableTimeout = MainClass.DisableTimeout;

            this.active_devices = new ActiveDevices();

            this.spectrometer_tv = new ScrolledSpectrometerTv(this);
            this.spectrometer_frame.Add(this.spectrometer_tv);
            this.spectrometer_frame.ShowAll();
            
            this.spectrometer_tv.SelectionChanged += this.OnSpectrometerTvSelectionChanged;
            this.spectrometer_tv.DoubleClicked    += this.OnSpectrometerTvDoubleClicked;

            this.auto_connect_checkbutton.Active = Settings.Current.AutoConnect;
            this.auto_select_checkbutton.Active  = Settings.Current.AutoSelect;            
        }

        private void InitializeDeviceListEvents()
        {
            this.DeviceList.DeviceAdded += this.OnDeviceAdded;
            this.DeviceList.DeviceRemoved += this.OnDeviceRemoved;
            this.DeviceList.DeviceStateChanged += this.OnDeviceStateChanged;

            this.DeviceList.DeviceException += this.OnDeviceException;
            this.DeviceList.DeviceWarning += this.OnDeviceWarning;


            RemoteSerialInterface.StateChanged += () =>
            {
                Integration.TrayIcon.SetTooltip(RemoteSerialInterface.SharedCount,
                                                RemoteSerialInterface.ConnectedCount);
            };

            this.DeviceList.ActivateQueues = true;


            foreach (IDevice device in this.DeviceList.Values)
            {
                this.OnDeviceAdded(this.DeviceList, device.Info.SerialNumber);
                this.OnDeviceRemoved(this.DeviceList, device.Info.SerialNumber, device.CurrentConfig.Name);
                this.OnDeviceStateChanged(device);
            }

            this.spectrometer_tv.InitDeviceListEvents();
        }

        private void InitializeFileTv()
        {
            List<string> paths   = Settings.Current.DataBrowserPaths;
            List<string> current = Settings.Current.DataBrowserPaths;
            
            int remove_count = 0;
            for (int ix=0; ix < paths.Count; ++ix)
            {
                if (false == string.IsNullOrEmpty(paths[ix]) && FileIO.ExistsDirectory(paths[ix]))
                {
                    try
                    {
                        this.AddFileTv(paths[ix]);
                    }
                    catch (Exception ex)
                    {
                        InfoMessageDialog.Show(null, Catalog.GetString("Cannot open directory: {0}"), ex.Message);
                        current.RemoveAt(ix-remove_count++);
                    }
                }
                else
                {
                    current.RemoveAt(ix-remove_count++);
                }
            }
            
            Settings.Current.DataBrowserPaths = current;
            
            this.preview_checkbutton.Active = UserInterface.Settings.Current.ShowPreviewInPlot;
            
            this.file_notebook.PopupEnable();
            this.file_notebook.Removed    += this.OnFileNotebookRemoved;
            this.file_notebook.SwitchPage += this.OnFileNotebookChangeCurrentPage;
        }

        private void InitializePlotTv()
        {
            this.plot_tv = new ScrolledPlotTv(this.plot_info_list);
            this.plot_tv.SelectionChanged                 += this.OnPlotTvSelectionChanged;
            this.plot_tv.SpectrumSelectionPositionChanged += this.OnPlotTvSpectrumSelectionPositionChanged;
            this.plot_tv.SpectrumDoubleClicked            += this.OnPlotTvSpectrumDoubleClicked;
            this.plot_tv.FigureDoubleClicked              += this.OnPlotTvFigureDoubleClicked;
            this.plot_tv_frame.Add(this.plot_tv);
            
            this.plot_tv.ZOrderChanged += this.UpdatePlot;
            
            this.legend_checkbutton.Active = UserInterface.Settings.Current.AlwaysShowLegend;
        }

        private void ShowProgress(double fraction)
        {
            this.progressbar.Fraction = fraction;
            this.progressbar.Show();
            UserInterface.Instance.ProcessEvents();
        }

        private void HideProgress()
        {
            this.progressbar.Hide();
            UserInterface.Instance.ProcessEvents();
        }

        private void AddFileTv(string path)
        {
            string name = Path.GetFileName(path);

            ScrolledFileTv tv = new ScrolledFileTv(path);
            NotebookLabel  nl = new NotebookLabel(name, path);
            Label  menu_label = new Label(name);

            nl.CloseClicked += tv.Close;
            
            tv.SpectrumDoubleClicked     += this.OnSpectrumDoubleClicked;
            tv.OpenAllClicked            += this.OnOpenAllClicked;
            tv.DirectoryDoubleClicked    += this.OnFileTvDirectoryDoubleClicked;
            tv.DirectorySelectionChanged += this.OnFileTvDirectorySelectionChanged;
            tv.SpectrumSelectionChanged  += this.OnFileTvSpectrumSelectionChanged;
            tv.Closed                    += this.UpdatePropertiesSpectrometer;
    
            int ix = this.file_notebook.AppendPageMenu(tv, nl, menu_label);
            this.file_notebook.Page = ix;
            
            this.preview_checkbutton.Sensitive = true;
        }

        private void InitializeToolbar()
        {
            MainClass.InitializeIconFactory();
            this.insert_text_button.StockId        = "hiperscan.text";
            this.insert_line_button.StockId        = "hiperscan.line";
            this.insert_rectangle_button.StockId   = "hiperscan.rectangle";
            this.insert_ellipse_button.StockId     = "hiperscan.ellipse";
            this.insert_arrow_button.StockId       = "hiperscan.arrow";
            this.insert_doublearrow_button.StockId = "hiperscan.doublearrow";
            this.edit_x_constraints_button.StockId = "hiperscan.x-constraints";

            this.StandardOutputCulture = Settings.Current.StandardOutputCulture;

            foreach (Widget w in this.standard_toolbar)
            {
                this.standard_toolbar_widgets.Add(w);
            }

            this.x_constraints_entry = new XConstraintsEntry(UserInterface.entry_error_text_color, 
                                                             UserInterface.entry_error_base_color)
            {
                TooltipText = Catalog.GetString("Constraints of X-axis")
            };
            this.x_constraints_entry.FocusInEvent += (o, args) =>
            {
                GLib.Timeout.Add(50, () =>
                {
                    if (this.x_constraints_entry.Text.Length > 0)
                        this.x_constraints_entry.SelectRegion(0, this.x_constraints_entry.Text.Length);
                    return false;
                });
            };

            this.x_constraints_entry.FocusOutEvent += this.OnXConstraintsFocusOut;
            this.x_constraints_entry.ReturnPressed += (sender, e) =>
            {
                this.OnXConstraintsFocusOut(this.x_constraints_entry, null);
            };

            this.x_constraints_align.Add(this.x_constraints_entry);
            this.x_constraints_entry._XConstraintsCompletion.Selected += (sender, e) =>
            {
                GLib.Timeout.Add(0, () =>
                {
                    this.OnXConstraintsFocusOut(this.x_constraints_entry, null);
                    return false;
                });
            };
        }
        
        private void InitializeMenu()
        {
            this.data_sources_item.Active = Settings.Current.DataSourcesVisible;
            this.properties_item.Active   = Settings.Current.PropertyEditorVisible;
            this.plot_browser_item.Active = Settings.Current.PlotBrowserVisible;
            
            Gtk.AboutDialog.SetUrlHook((dialog, link) => this.ShowUri(link));
            
            if (Settings.Current.ReopenPaths == null    || 
                Settings.Current.ReopenPaths.Count == 0 || 
                string.IsNullOrEmpty(Settings.Current.ReopenPaths[0]))
            {
                this.reopen_item.Sensitive = false;
            }

            this.extensions_menu.AccelGroup = this.gui.EnsureAccel();
            
            if (MainClass.OS == MainClass.OSType.MacOSX)
                MacIntegration.InstallGlobalMenu(this.menubar, this.quit_item);
            
            switch (Settings.Current.AxisType)
            {
                
            case AxisType.Wavelength:
                this.axis_type_wavelength_item.Active = true;
                break;
                
            case AxisType.Wavecount:
                this.axis_type_wavecount_item.Active = true;
                break;
                
            }
        }
        
        private void InitializeFileTypes()
        {
            this.file_types.Add("quickstep", new FileTypeInfo[]
            { 
                new CsvFileTypeInfo(), 
                new CsvContainerFileTypeInfo() 
            });
        }
        
        public string GenerateFilename(Spectrum spectrum)
        {
            string fname = Catalog.GetString(DEFAULT_SPECTRUM_FILENAME);
            
            if (spectrum.SpectrumType == SpectrumType.Single ||
                spectrum.SpectrumType == SpectrumType.Stream ||
                spectrum.SpectrumType == SpectrumType.ProcessedFluidSpecimen ||
                spectrum.SpectrumType == SpectrumType.ProcessedSolidSpecimen)
            {
                fname = spectrum.Id.Remove(0, 2);
            }

            if (spectrum.IsFromFile && spectrum.Id.Length > 12)
                fname = Path.GetFileName(spectrum.Id.Remove(0, 12));
            
            return fname;
        }
        
        private string AnticipateFilename(string last_fname)
        {
            Console.WriteLine("Anticipating filename from \"{0}\":", last_fname);
            Regex regex = new Regex(@"_[0-9]+\..*$", RegexOptions.IgnoreCase);
            Match match = regex.Match(last_fname);
            if (match.Success == false)
                return null;
            
            Group g = match.Groups[0];
            
            regex = new Regex(@"[0-9]+");
            match = regex.Match(g.ToString());
            if (match.Success == false)
                return last_fname;

            g = match.Groups[0];
            
            int len = g.ToString().Length;
            int val = int.Parse(g.ToString());

            string new_fname = last_fname.Replace($"_{g.ToString()}.",
                                                  $"_{(++val).ToString("D" + len.ToString())}.");
            
            Console.WriteLine(new_fname);
            return new_fname;
        }
        
        public string SaveNewSpectrum(Spectrum spectrum, string fname)
        {
            FileTypeInfo file_type = new CsvFileTypeInfo();
            bool save_all = false;
            bool again    = false;
            bool? cancel  = false;
            OutputCulture output_culture = this.StandardOutputCulture;
            return this.SaveNewSpectrum(spectrum,
                                        fname, 
                                        ref file_type,
                                        ref save_all, 
                                        ref again, 
                                        ref cancel, 
                                        ref output_culture);
        }
        
        private string SaveNewSpectrum(Spectrum spectrum,
                                       string fname,
                                       ref FileTypeInfo file_type,
                                       ref bool save_all,
                                       ref bool again,
                                       ref bool? cancel,
                                       ref OutputCulture output_culture)
        {
            const int SAVE_ALL = 1000;
            using (Dialog.FileChooserDialog chooser = new Dialog.FileChooserDialog(Catalog.GetString("Specify file name to save a new spectrum"), 
                                                                                   this.MainWindow,
                                                                                   FileChooserAction.Save,
                                                                                   Stock.Save,
                                                                                   ResponseType.Ok,
                                                                                   Catalog.GetString("Save a_ll"),
                                                                                   SAVE_ALL,
                                                                                   Stock.Cancel,
                                                                                   ResponseType.Cancel))
            {
                foreach (FileTypeInfo type in this.FileTypes)
                {
                    if (type.RwType != FileRwType.ReadOnly && !type.IsContainer)
                        chooser.AddFilter(this.GetFileFilter(type), type.Extensions.First());
                }
                chooser._CurrentName  = fname;
                chooser.Filter = this.GetFileFilter(file_type);
                chooser.OutputCulture = output_culture;
                chooser.ActionArea.Children[4].Sensitive = save_all;
                
                if (string.IsNullOrEmpty(Settings.Current.SaveFilePath) == false)
                    chooser.SetCurrentFolder(Settings.Current.SaveFilePath);
                else
                    chooser.SetCurrentFolder(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
                
                chooser.DoOverwriteConfirmation = true;
                string actual_fname = string.Empty;
                save_all = false;
                again = false;

                try
                {
                    int choice = chooser._Run();
                    file_type = this.GetFileTypeInfo(chooser.Filter, false, false);

                    output_culture = chooser.OutputCulture;
                    
                    switch (choice)
                    {
                        
                    case (int)ResponseType.Ok:
                        chooser.Hide();
                        file_type.Write(spectrum, chooser.Filename, this.OutputCultureInfo(chooser.OutputCulture));
                        spectrum.FileInfo = new FileInfo(chooser.Filename);
                        Settings.Current.SaveFilePath = chooser.CurrentFolder;
                        actual_fname = Path.GetFileName(chooser.Filename);
                        break;
                        
                    case SAVE_ALL:
                        save_all = true;
                        FileInfo file = new FileInfo(chooser.Filename);
                        if (file.Exists == false)
                            goto case (int)ResponseType.Ok;
                        chooser.Hide();
                        var dialog = new OverwriteSpectrumDialog(this.MainWindow, Path.GetFileName(chooser.Filename));
                        choice = dialog.Run();
                        dialog.Destroy();
                        if ((ResponseType)choice == ResponseType.Yes)
                            goto case (int)ResponseType.Ok;
                        actual_fname = Path.GetFileName(chooser.Filename);
                        again = true;
                        break;

                    case (int)ResponseType.Cancel:
                        if (cancel != null)
                            break;
                        Dialog.MessageDialog cancel_dialog =
                                  new Dialog.MessageDialog(chooser,
                                                           DialogFlags.Modal,
                                                           MessageType.Question,
                                                           ButtonsType.YesNo,
                                                           Catalog.GetString("Do you want to cancel all pending file save operations?"));
                        cancel = cancel_dialog.Run() == (int)ResponseType.Yes;
                        cancel_dialog.Destroy();
                        break;

                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                    InfoMessageDialog.Show(this.MainWindow,
                                           Catalog.GetString("Error while saving spectrum file: {0}"),
                                           ex.Message);
                }
                finally
                {
                    chooser.Destroy();
                }
                
                return actual_fname;
            }
        }

        public void OpenFileBrowser(string path)
        {
            UserInterface.OpenFileBrowser(this.MainWindow, path);
        }
        
        public static void OpenFileBrowser(Window parent, string path)
        {
            System.Diagnostics.Process process = new System.Diagnostics.Process();

            process.StartInfo.CreateNoWindow  = true;
            process.StartInfo.UseShellExecute = false;

            if (MainClass.OS == MainClass.OSType.Windows)
            {
                // start explorer
                process.StartInfo.Arguments = $"/e,\"{path}\"";
                process.StartInfo.FileName = "explorer.exe";
                
                try
                {
                    process.Start();
                    return;
                }
                catch 
                {
                    try 
                    {
                        string windows_path =
                            Directory.GetParent(Environment.GetFolderPath(Environment.SpecialFolder.System)).FullName;
                        process.StartInfo.FileName = Path.Combine(windows_path, "explorer.exe");
                        process.Start();
                        return;
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(string.Format(Catalog.GetString("Cannot start Windows Explorer: {0}"), ex.Message), ex);
                    }
                }
            }
            else
            {
                // try to start one of Linux' file browsers
                process.StartInfo.Arguments = "\"" + path + "\"";
                string[] filemanagers = { "nautilus", "dolphin", "konqueror", "thunar"};
                
                foreach (string filemanager in filemanagers)
                {
                    process.StartInfo.FileName = filemanager;
                    try
                    {
                        process.Start();
                        return;
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(string.Format(Catalog.GetString("Cannot start file browser: {0}"), ex.Message), ex);
                    }
                }
            }

            InfoMessageDialog.Show(parent, Catalog.GetString("Cannot start file browser."));
        }
        
        private void AttachPropertyEditor()
        {
            this.property_handlebox.Remove(this.property_frame);
            
            this.property_vbox.Remove(this.property_handlebox);
            this.property_handlebox.Destroy();

            HandleBox hb = new HandleBox
            {
                ShadowType     = ShadowType.In,
                HandlePosition = PositionType.Top,
                SnapEdge       = PositionType.Bottom,
                SnapEdgeSet    = true
            };
            hb.Add(this.property_frame);
            hb.ChildAttached += this.OnPropertyEditorAttached;
            hb.ChildDetached += this.OnPropertyEditorDetached;
            this.property_handlebox = hb;
            
            this.attach_property_editor_button.Sensitive = false;
            
            this.property_handlebox.ShowAll();
            this.property_frame.ShowAll();
            
            this.property_vbox.PackEnd(this.property_handlebox, false, false, 0);
        }
        
        internal void ShowUri(string uri)
        {
            try
            {
                Process.OpenUri(uri);
            }
            catch (Exception ex)
            {
                InfoMessageDialog.Show(this.MainWindow,
                                       Catalog.GetString("Cannot open URI '{0}': {1}"), 
                                       uri, 
                                       ex.Message);
            }
        }

        internal void SetWatchCursor(object o, EventArgs e)
        {
            if (this.MainWindow?.GdkWindow == null)
                return;

            this.MainWindow.GdkWindow.Cursor = new Gdk.Cursor(Gdk.CursorType.Watch);
        }

        internal void SetArrowCursor(object o, EventArgs e)
        {
            if (this.MainWindow?.GdkWindow == null)
                return;
            this.MainWindow.GdkWindow.Cursor = new Gdk.Cursor(Gdk.CursorType.Arrow);
        }

        public void ProcessEvents()
        {
            if (this.AllowProcessEvents == false)
                return;

            if (Thread.CurrentThread.GetHashCode() != MainClass.MainThreadId)
                return;

            lock (UserInterface.Instance)
                    UserInterface.__ProcessEvents();
        }

        public bool CheckForUnsavedSpectra()
        {
            if (MainClass.EnableDebugging)
                return true;
            
            bool unsaved = false;
            foreach (Spectrum spectrum in this.CurrentSpectra.Values)
            {
                if (spectrum.FileInfo == null && 
                    spectrum.SpectrumType != SpectrumType.QuickStream)
                {
                    unsaved = true;
                    break;
                }
            }
            
            if (unsaved)
            {
                var dialog = new DiscardOnExitDialog(this.MainWindow);
                ResponseType response = (ResponseType)dialog.Run();
                dialog.Destroy();
                if (response != ResponseType.Yes)
                    return false;
            }
            
            return true;
        }
        
        public void Quit()
        {
            MainClass.Quit(false);
        }
        
        public void Quit(bool force)
        {
            MainClass.Quit(force);
        }

        private static void __ProcessEvents()
        {
            if (Thread.CurrentThread.GetHashCode() == MainClass.MainThreadId)
            {
                int count = 0;
                while (Application.EventsPending())
                {
                    if (++count > 200)
                    {
                        Console.WriteLine("__ProcessEvents(): reached break condition (too many events pending)");
                        break;
                    }
                    Application.RunIteration();
                }
            }
        }
        
        private int OnKeyPressed(Widget w, Gdk.EventKey e)
        {
            if (e.Type != Gdk.EventType.KeyPress)
                return 0;
            
            try
            {
                switch (e.Key)
                {
                    
                case Gdk.Key.Escape:
                    if (this.CancelAnnotations())
                        this.ScrolledPlot.Plot.AsyncRefresh(true);
                    break;
                    
                case Gdk.Key.Tab:
                case Gdk.Key.Page_Down:
                    if ((e.State & Gdk.ModifierType.ControlMask) == Gdk.ModifierType.ControlMask)
                    {
                        if (this.plot_notebook.CurrentPage >= this.plot_notebook.NPages-2)
                            this.plot_notebook.CurrentPage = 0;
                        else
                            this.plot_notebook.CurrentPage++;
                        return 1;
                    }
                    break;
                    
                case Gdk.Key.Page_Up:
                    if ((e.State & Gdk.ModifierType.ControlMask) == Gdk.ModifierType.ControlMask)
                    {
                        if (this.plot_notebook.CurrentPage == 0)
                            this.plot_notebook.CurrentPage = this.plot_notebook.NPages-2;
                        else
                            this.plot_notebook.CurrentPage--;
                        return 1;
                    }
                    break;
                
                case Gdk.Key.F4:
                    if ((e.State & Gdk.ModifierType.ControlMask) == Gdk.ModifierType.ControlMask)
                    {
                        this.RemovePlotTab(this.plot_info_list.Current);
                        return 1;
                    }
                    break;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error in global key snooper: " + ex.Message);
            }
            
            return 0;
        }
        
        internal void OnToolCloseClicked(object o, System.EventArgs e)
        {
            Type t = o.GetType();
            
            if (t.FullName == "Gtk.ToolButton" || t.FullName == "Gtk.MenuItem")
            {
                this.DataSourcesVisible    = false;
                this.PropertyEditorVisible = false;
                this.PlotBrowserVisible    = false;
                return;
            }
            
            Button b = o as Button;
            
            switch (b.Name)
            {
                
            case "close_data_sources_button":
                this.DataSourcesVisible = false;
                break;
                
            case "close_property_editor_button":
                this.PropertyEditorVisible = false;
                break;
                
            case "close_plot_browser_button":
                this.PlotBrowserVisible = false;
                break;
                                
            default:
                throw new ArgumentException("Unknown CloseButton.");
                
            }
            b.InButton = false;
        }

        internal void OnToolAttachClicked(object o, System.EventArgs e)
        {
            Button b = o as Button;
            
            switch (b.Name)
            {
                
            case "attach_property_editor_button":
                this.AttachPropertyEditor();
                break;
                                
            default:
                throw new ArgumentException("Unknown AttachButton.");
                
            }
            b.InButton = false;
        }

        internal void OnPropertyEditorDetached(object o, Gtk.ChildDetachedArgs e)
        {
            this.attach_property_editor_button.Sensitive = true;
        }

        internal void OnPropertyEditorAttached(object o, Gtk.ChildAttachedArgs e)
        {
            this.attach_property_editor_button.Sensitive = false;
        }

        public FileTypeInfo[] FileTypes
        {
            get 
            {
                var file_type_infos = this.file_types.Values.SelectMany(i => i).ToList();

                file_type_infos.Sort((lhs, rhs) =>
                {
                    if (lhs.Name.ToLower().StartsWith("quickstep", StringComparison.InvariantCulture))
                        return -1;
                    if (rhs.Name.ToLower().StartsWith("quickstep", StringComparison.InvariantCulture))
                        return 1;
                    return string.Compare(lhs.Name, rhs.Name, StringComparison.InvariantCulture);
                });
                
                return file_type_infos.ToArray();
            }
        }
        
        public FileFilter GetFileFilter(FileTypeInfo file_type_info)
        {
            // we need to keep the object references to set the file filter later
            if (this.file_filters.ContainsKey(file_type_info.Name) == false)
            {
                FileFilter filter = new FileFilter
                {
                    Name = file_type_info.Name
                };
                foreach (string extension in file_type_info.Extensions)
                {
                    filter.AddPattern($"*.{extension.ToLower()}");
                    filter.AddPattern($"*.{extension.ToUpper()}");
                }
                this.file_filters.Add(filter.Name, filter);
            }
            return this.file_filters[file_type_info.Name];
        }
        
        public FileTypeInfo GetFileTypeInfo(FileFilter filter, bool read, bool force_container)
        {
            foreach (FileTypeInfo file_type in this.FileTypes)
            {
                if (read && file_type.RwType == FileRwType.WriteOnly)
                    continue;

                if (force_container && file_type.IsContainer == false)
                    continue;

                if (filter.Name == file_type.Name)
                    return file_type;
            }
            return new CsvFileTypeInfo();
        }

        public bool IsGuiThread
        {
            get
            {
                return this.GuiThreadId == System.Threading.Thread.CurrentThread.ManagedThreadId;
            }
        }

        private int GuiThreadId { get; set; } = -1;

        public ScrolledPlot ScrolledPlot      => this.plot_info_list.Current.ScrolledPlot;
        public Widget GUI                     => this.ui_vbox;
        public ICurrentSpectra CurrentSpectra => this.plot_info_list.Current.CurrentSpectra;
        public Gdk.Color BackgroundColor      => Rc.GetStyle(this.ScrolledPlot.Plot.GtkWidget).Background(StateType.Normal);
        public AccelGroup AccelGroup          => this.gui.EnsureAccel();
        public Menu SpectrometerMenu          => this.spectrometer_menu;
        public Menu EditMenu                  => this.edit_menu;
        public string ConfigurationDirPath    => UserInterface.ConfigDirPath;
        public string VersionInfo             => MainClass.VersionInfo;

        public Window MainWindow               { get; }
        public string [] Args                  { get; set; }
        public bool WindowsProfileChanged         { get; set; } = false;
        public string MainWindowTitelExtension { get; set; } = string.Empty;

        internal static Queues Queues          { get; private set; }
        internal static List<Thread> Threads   { get; private set; }
        internal static UserInterface Instance { get; private set; }
        public int ReleaseCandidateVersion { get; set; } = -1;

        public bool AllowProcessEvents { get; set; } = true;
    }
}
