// SpectrometerTreeview.cs created with MonoDevelop
// User: klose at 13:47 15.01.2009
// CVS release: $Id: SpectrometerTv.cs,v 1.27 2011-04-19 12:36:58 klose Exp $
//
//    QuickStep: Acquire, view and identify spectra 
//    Copyright (C) 2009  Thomas Klose, Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Collections;
using System.Collections.Generic;

using Gtk;

using Hiperscan.Common;
using Hiperscan.SGS;
using Hiperscan.SGS.Benchtop;
using Hiperscan.SGS.Common;
using Hiperscan.SGS.SerialInterface;
using Hiperscan.Unix;
using System.Threading;
using Hiperscan.Extensions;
using System.Diagnostics;

namespace Hiperscan.QuickStep.Treeview
{
    internal class SpectrometerTv : Gtk.TreeView
    {
        public delegate void DoubleClickedHandler(SpectrometerTv tv);
        public event DoubleClickedHandler DoubleClicked;

        public delegate void Button3ClickedHandler(SpectrometerTv tv);
        public event Button3ClickedHandler Button3Clicked;


        public SpectrometerTv(TreeStore store) : base(store) { }

        protected override bool OnButtonPressEvent(Gdk.EventButton e)
        {
            if ((e.Button == 3) && (this.Button3Clicked != null))
                this.Button3Clicked(this);

            if ((e.Button == 1) && (e.Type == Gdk.EventType.TwoButtonPress) && (this.DoubleClicked != null))
                this.DoubleClicked(this);

            return base.OnButtonPressEvent(e);
        }

        //        protected override bool OnExpandCollapseCursorRow(bool logical, bool expand, bool open_all)
        //        {
        //            return base.OnExpandCollapseCursorRow(logical, expand, open_all);
        //        }
    }

    public class ScrolledSpectrometerTv : Gtk.ScrolledWindow
    {
        private enum Store
        {
            Device,
            Online,
            State,
            Temperature,
            Visible,
            Mode
        }

        private SpectrometerTv tv;
        private TreeStore store;

        private readonly ArrayList columns = new ArrayList();

        private readonly Dictionary<string, IDevice> devices = new Dictionary<string, IDevice>();
        private readonly Dictionary<string, TreePath> paths = new Dictionary<string, TreePath>();
        private readonly Dictionary<string, TreePath> parents = new Dictionary<string, TreePath>();

        private bool auto_connect = false;
        private bool auto_select = false;
        private bool connecting = false;

        public delegate void SelectionChangedHandler(List<IDevice> devices);
        public event SelectionChangedHandler SelectionChanged;

        public delegate void DoubleClickedHandler(List<IDevice> devices);
        public event DoubleClickedHandler DoubleClicked;

        private delegate void ConnectDelegate(IDevice dev, int delay);

        
        public ScrolledSpectrometerTv(IHost host) : base()
        {
            this.Host = host;
            this.store = new TreeStore(typeof(string),
                                       typeof(bool),
                                       typeof(string),
                                       typeof(string),
                                       typeof(bool),
                                       typeof(CellRendererMode));
            this.tv = new SpectrometerTv(store)
            {
                RulesHint = false
            };
            this.tv.Selection.Mode = SelectionMode.Multiple;

            this.tv.Selection.Changed += new EventHandler(this.OnSelectionChanged);

            CellRendererText text;
            CellRendererToggle toggle;
            TreeViewColumn column;

            // device column
            text = new CellRendererText
            {
                Xalign = 0f
            };
            this.columns.Add(text);
            column = new TreeViewColumn(Catalog.GetString("Device"), text,
                                        "mode", Store.Mode,
                                        "markup", Store.Device);
            column.SetCellDataFunc(text, new TreeCellDataFunc(this.RenderEntry));
            column.Resizable = true;
            this.tv.InsertColumn(column, (int)Store.Device);

            // online column
            toggle = new CellRendererToggle
            {
                Xalign = 0.5f
            };
            this.columns.Add(toggle);
            toggle.Toggled += new ToggledHandler(OnToggled);
            column = new TreeViewColumn(string.Empty, toggle,
                                        "active", (int)Store.Online,
                                        "visible", (int)Store.Visible,
                                        "mode", (int)Store.Mode);
            //            column.SetCellDataFunc(toggle, new TreeCellDataFunc(this.RenderEntry));
            column.Widget = new Image(Stock.Connect, IconSize.Menu);
            column.Widget.Show();
            this.tv.InsertColumn(column, (int)Store.Online);

            // state column
            text = new CellRendererText
            {
                Xalign = 0f
            };
            this.columns.Add(text);
            column = new TreeViewColumn(Catalog.GetString("Status"), text,
                                        "mode", Store.Mode,
                                        "text", Store.State);
            column.SetCellDataFunc(text, new TreeCellDataFunc(this.RenderEntry));
            this.tv.InsertColumn(column, (int)Store.State);

            // state temperature
            text = new CellRendererText
            {
                Xalign = 0f
            };
            this.columns.Add(text);
            column = new TreeViewColumn(Catalog.GetString("ϑ/°C"), text,
                                        "mode", Store.Mode,
                                        "text", Store.Temperature);
            column.SetCellDataFunc(text, new TreeCellDataFunc(this.RenderEntry));
            this.tv.InsertColumn(column, (int)Store.Temperature);

            tv.Model = this.store;
            tv.HeadersVisible = true;

            this.tv.WidthRequest = 210;
            this.tv.HeightRequest = 120;
            this.Add(this.tv);

            this.tv.DoubleClicked += this.OnDoubleClicked;
            this.tv.Button3Clicked += this.OnButton3Clicked;
        }

        public void InitDeviceListEvents()
        {
            this.Host.DeviceList.Changed += this.UpdateList;
            this.Host.DeviceList.DeviceStateChanged += this.UpdateDevice;

            this.UpdateList(this.Host.DeviceList);
            foreach (IDevice device in this.Host.DeviceList.Values)
                this.UpdateDevice(device);
        }

        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.Sensitive = false;
            else
                cell.Sensitive = true;
        }


        internal void UpdateList(DeviceList list)
        {
            if (this.Host.IsGuiThread)
            {
                try
                {
                    this.TVSync.WaitOne();
                    {
                        UserInterface.Instance.AllowProcessEvents = false;
                        List<IDevice> current_devices = new List<IDevice>(this.devices.Values);
                        List<string> current_unavailable = list.Blacklist;

                        this.store.Clear();
                        this.devices.Clear();
                        this.paths.Clear();
                        this.parents.Clear();

                        lock (list)
                        {
                            List<string> keys = new List<string>(list.Serials);
                            keys.Sort();

                            foreach (string key in keys)
                            {
                                IDevice dev = list[key];

                                TreeIter parent;
                                string type = StringEnum.GetStringValue(dev.Info.DeviceType);
                                if (dev.Info.IsRemote)
                                    parent = this.AddParentNode(type + "@" + dev.Info.RemoteSerialInterfaceClient.Server);
                                else
                                    parent = this.AddParentNode(type);

                                Console.WriteLine("Adding device to Spectrometer TreeView: " + dev.CurrentConfig.Name);
                                TreeIter iter = this.store.AppendValues(parent,
                                                                        dev.CurrentConfig.Name,
                                                                        dev.GetState() != Spectrometer.State.Disconnected,
                                                                        dev.GetState().ToString(),
                                                                        Catalog.GetString("n/a"),
                                                                        true,
                                                                        CellRendererMode.Activatable);
                                this.AddDeviceNode(iter, dev);
                                this.UpdateDevice(dev);
                            }

                            foreach (string key in current_unavailable)
                            {
                                TreeIter parent = this.AddParentNode("Unavailable", CellRendererMode.Inert);
                                Console.WriteLine("Adding unavailable device to Spectrometer TreeView: " + key);
                                TreeIter iter = this.store.AppendValues(parent,
                                                                        key,
                                                                        false,
                                                                        "Unavailable",
                                                                        Catalog.GetString("n/a"),
                                                                        true,
                                                                        CellRendererMode.Inert);
                            }
                        }

                        this.tv.ExpandAll();

                        if (this.auto_connect || MainClass.AutoSharePorts.Count > 0)
                        {
                            int delay = 0;
                            foreach (IDevice dev in this.devices.Values)
                            {
                                if (!current_devices.Contains(dev))
                                {
                                    ConnectDelegate dlg = new ConnectDelegate(this.Connect);
                                    dlg.BeginInvoke(dev, delay, null, null);
                                    //delay =+ 200;

                                    // select new devices
                                    if (this.auto_select)
                                    {
                                        TreeIter iter = this.GetIter(dev);
                                        if (!iter.Equals(TreeIter.Zero))
                                            this.tv.Selection.SelectIter(iter);
                                    }
                                }
                            }
                        }

                    }
                }
                finally
                {
                    UserInterface.Instance.AllowProcessEvents = true;
                    this.TVSync.Release();
                }
            }
            else
            {
                Application.Invoke((sender, e) =>
                {
                    this.UpdateList(this.Host.DeviceList);
                });
            }

        }

        private Semaphore TVSync { get; set; } = new Semaphore(1, 1);
        private Semaphore TVInvokeSync { get; set; } = new Semaphore(1, 1);


        private void Connect(IDevice dev, int delay)
        {
            this.connecting = true;
            Thread.Sleep(delay);

            Console.WriteLine("Open device " + dev);
            try
            {
                dev.Open();

                if (dev.IsFinder && dev.Info.DeviceType != DeviceType.CalibrationDevice)
                {
                    dev.SetFinderLightSource(false, null);
                    dev.SetFinderPosition(FinderWheelPosition.Standby, null);
                }
            }
            catch (NullReferenceException) {/* device may be suddenly disconnected*/}
            catch (Exception ex)
            {
                if (dev.GetState() == Spectrometer.State.Unprogrammed)
                    return;

                string msg = string.Format(Catalog.GetString("Cannot connect device with serial number {0}: {1}"),
                                           dev.Info.SerialNumber, ex.Message);

                UserInterface.Queues.AddToMessageQueue(ex.Message, ex);
                Console.WriteLine("Failed.");
            }
            finally
            {
                this.connecting = false;
            }
        }

        internal void UpdateDevice(IDevice dev)
        {
            if (this.Host.IsGuiThread)
            {
                TreeIter iter = this.GetIter(dev);
                if (iter.Equals(TreeIter.Zero))
                    return;

                Spectrometer.State state = dev.GetState();
                this.store.SetValues(iter,
                                     dev.CurrentConfig.Name,
                                     state != Spectrometer.State.Disconnected,
                                     state.ToString(),
                                     double.IsNaN(dev.LastTemperature) ? Catalog.GetString("n/a") : string.Format("{0:F1}", dev.LastTemperature),
                                     true,
                                     CellRendererMode.Activatable);

                if (dev.IsFinder && state != Spectrometer.State.Shared && state != Spectrometer.State.Disconnected)
                {

                    if (!this.store.IterNthChild(out TreeIter position_iter, iter, 0))
                    {

                        position_iter = this.store.AppendValues(iter,
                                                                  dev.FinderPositionTypeName,
                                                                  false,
                                                                  dev.FinderPositionName,
                                                                  string.Empty,
                                                                  false,
                                                                  CellRendererMode.Activatable);

                        this.tv.ExpandRow(this.store.GetPath(iter), true);
                    }

                    string status = dev.FinderLightSourceState ? Catalog.GetString("On") : Catalog.GetString("Off");

                    if (!this.store.IterNthChild(out TreeIter light_source_iter, iter, 1))
                    {
                        light_source_iter = this.store.AppendValues(iter,
                                                                    Catalog.GetString("Light source"),
                                                                    false,
                                                                    status,
                                                                    string.Empty,
                                                                    false,
                                                                    CellRendererMode.Activatable);
                        this.tv.ExpandRow(this.store.GetPath(iter), true);
                    }

                    this.store.SetValues(position_iter,
                                             dev.FinderPositionTypeName,
                                             false,
                                             dev.FinderPositionName,
                                             string.Empty,
                                             false,
                                             CellRendererMode.Activatable);

                    this.store.SetValues(light_source_iter,
                                         Catalog.GetString("Light source"),
                                         false,
                                         status,
                                         string.Empty,
                                         false,
                                         CellRendererMode.Activatable);
                }
                else if (dev.IsFinder && this.store.IterHasChild(iter))
                {
                    TreeIter next;
                    bool done = false;
                    this.store.IterChildren(out TreeIter child, iter);
                    do
                    {
                        next = child;
                        done = this.store.IterNext(ref next) == false;
                        this.store.Remove(ref child);
                        child = next;
                    }
                    while (done == false);
                }
 
                UserInterface.Instance.ProcessEvents();
            }
            else
            {
                Application.Invoke((sender, e) =>
                {
                    this.UpdateDevice(dev);
                });
            }
        }

        private List<IDevice> SelectDevices(TreeSelection selection)
        {
            List<IDevice> selected_devices = new List<IDevice>();

            foreach (TreePath path in selection.GetSelectedRows(out TreeModel model))
            {
                if (this.store.GetIterFromString(out TreeIter iter, path.ToString()))
                {
                    IDevice dev;

                    if (this.IsParent(iter))
                    {
                        // If a Parent iter (a.k.a. Device Group) is selected,
                        // we want to select all children (a.k.a. instruments belonging to that group).
                        int count = this.store.IterNChildren(iter);
                        Console.WriteLine("Group selected: " + count + " sub items");

                        for (int ix = 0; ix < count; ++ix)
                        {
                            if (this.store.IterNthChild(out TreeIter child, iter, ix))
                            {
                                // Since this is potentially called in the context
                                // of the selection changed event handler, we rather
                                // execute the update the update into the event loop
                                // in order to avoid recursive execution.
                                Application.Invoke((sender, e) =>
                                {
                                    this.tv.Selection.SelectIter(child);
                                });
                            }
                        }
                        continue;
                    }

                    dev = this.GetDevice(iter);
                    if (dev != null)
                    {
                        Console.WriteLine("Selected: " + dev);
                        selected_devices.Add(dev);
                    }
                }
            }

            this.SelectionChanged(selected_devices);

            return selected_devices;
        }

        public bool SelectDevice(IDevice dev)
        {
            try
            {
                TreeIter iter = this.GetIter(dev);
                if (iter.Equals(TreeIter.Zero))
                    return false;
                this.tv.Selection.UnselectAll();
                this.tv.Selection.SelectIter(iter);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        private void OnToggled(object o, Gtk.ToggledArgs e)
        {
            int column = this.columns.IndexOf(o);
            if (this.store.GetIterFromString(out TreeIter iter, e.Path))
            {
                bool val = (bool)store.GetValue(iter, column);
                IDevice dev = this.GetDevice(iter);

                if (dev == null || this.connecting)
                    return;

                if (val == false)
                {
                    ConnectDelegate dlg = new ConnectDelegate(this.Connect);
                    dlg.BeginInvoke(dev, 0, null, null);
                }
                else
                {
                    Console.WriteLine("Close device " + dev);
                    dev.Close();
                }
            }
        }

        private void OnDoubleClicked(SpectrometerTv tv)
        {
            this.DoubleClicked?.Invoke(this.SelectDevices(tv.Selection));
        }

        private void OnButton3Clicked(SpectrometerTv tv)
        {
            MainWindow.UI.SpectrometerMenu.Popup();
        }

        private void OnSelectionChanged(object o, System.EventArgs e)
        {
            TreeSelection selection = o as TreeSelection;

            foreach (TreePath path in selection.GetSelectedRows())
            {
                this.store.GetIterFromString(out TreeIter iter, path.ToString());
                if (!(bool)this.store.GetValue(iter, (int)Store.Visible) && !this.store.IterHasChild(iter))
                {
                    selection.UnselectIter(iter);

                    if (this.store.IterParent(out TreeIter parent, iter))
                        selection.SelectIter(parent);
                }
            }

            List<IDevice> selected_devices = this.SelectDevices(selection);

            this.SelectionChanged?.Invoke(selected_devices);
        }

        private void AddDeviceNode(TreeIter iter, IDevice dev)
        {
            TreePath path = this.store.GetPath(iter);
            this.paths.Add(dev.Info.SerialNumber, path);
            this.devices.Add(path.ToString(), dev);
        }

        private TreeIter AddParentNode(string name, CellRendererMode cell_renderer_mode = CellRendererMode.Activatable)
        {
            TreeIter iter;

            if (this.parents.ContainsKey(name))
            {
                this.store.GetIter(out iter, this.parents[name]);
            }
            else
            {
                iter = this.store.AppendValues("<b>" + name + "</b>", false, string.Empty, string.Empty, false, cell_renderer_mode);
                this.parents.Add(name, this.store.GetPath(iter));
            }
            return iter;
        }

        private TreeIter GetIter(IDevice dev)
        {
            if (!this.paths.ContainsKey(dev.Info.SerialNumber))
                return TreeIter.Zero;
            TreePath path = this.paths[dev.Info.SerialNumber];
            TreeIter iter;
            this.store.GetIterFromString(out iter, path.ToString());
            return iter;
        }

        private IDevice GetDevice(TreeIter iter)
        {
            TreePath path = this.store.GetPath(iter);
            if (!this.devices.ContainsKey(path.ToString()))
                return null;
            return this.devices[path.ToString()];
        }

        private bool IsParent(TreeIter iter)
        {
            TreePath path = this.store.GetPath(iter);
            return this.parents.ContainsValue(path);
        }

        public bool AutoConnect
        {
            get { return this.auto_connect; }
            set { this.auto_connect = value; }
        }

        public bool AutoSelect
        {
            get { return this.auto_select; }
            set { this.auto_select = value; }
        }

        private IHost Host { get; set; }
    }
}
