// ScrolledHSPlot.cs created with MonoDevelop
// User: klose at 10:03 27.12.2008
// CVS release: $Id: ScrolledPlot.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.Generic;

using Gtk;

using Hiperscan.NPlotEngine;
using Hiperscan.NPlotEngine.Overlay.Annotation;
using Hiperscan.NPlotEngine.Overlay.Selection;
using Hiperscan.QuickStep.Zoombar;
using Hiperscan.Spectroscopy.IO;


namespace Hiperscan.QuickStep.Figure
{

    public class ScrolledPlot : Gtk.Table
    {
        private const int MIN_SELECTION_LENGTH = 5;
        private const double EPSILON = 1e-30;

        private double current_x_zoom = 1.0;
        private double current_y_zoom = 1.0;
        private double current_x_pan  = 0.5;
        private double current_y_pan  = 0.5;
        
        private DragInfo button1_drag_info = null;
        private DragInfo button2_drag_info = null;

        private readonly PlotInfo plot_info;

        public delegate void PointerCoordsChangedHandler(double x, double y);
        public event         PointerCoordsChangedHandler PointerCoordsChanged;
        
        public delegate void XAxesReversedHandler(IPlotEngine plot);
        public event         XAxesReversedHandler XAxesReversed;
        

        public ScrolledPlot(PlotInfo plot_info) : base(2, 2, false)
        {
            this.Plot = new NPlotEngine.NPlotEngine();

            this.plot_info = plot_info;

            this.HZoombar = new HZoombar();
            this.VZoombar = new VZoombar();
            
            // set event handlers
            this.HZoombar.AsyncChanged += this.OnHAsyncChanged;
            this.HZoombar.Changed      += this.OnHChanged;
            this.HZoombar.AsyncChangedCallbackInvoked += this.OnHCallbackInvoked;

            this.VZoombar.AsyncChanged += this.OnVAsyncChanged;
            this.VZoombar.Changed      += this.OnVChanged;
            this.VZoombar.AsyncChangedCallbackInvoked += this.OnVCallbackInvoked;
            
            this.Plot.DataSetAdded  += this.OnDataSetAdded;
            this.Plot.XAxesReversed += this.OnXAxesReversed;

            // pack widgets in table
            this.Attach(this.Plot.GtkWidget, 0, 1, 0, 1);
            this.Attach(this.HZoombar,  0, 1, 1, 2, AttachOptions.Fill, AttachOptions.Fill, 0, 0);
            this.Attach(this.VZoombar,  1, 2, 0, 1, AttachOptions.Fill, AttachOptions.Fill, 0, 0);
            
            this.ZoomType = ZoomType.Proportional;
            this.HasPopupDialog = false;
        }
        
        ~ScrolledPlot()
        {
            ((IDisposable)this.Plot).Dispose();
        }

        public void ZoomIn()
        {
            double new_zoom = this.XZoom * 1.2;
            
            if (new_zoom > this.MaxZoom)
                new_zoom = this.MaxZoom;
            
            this.XZoom = new_zoom;

            new_zoom = this.YZoom * 1.2;
            
            if (new_zoom > this.MaxZoom)
                new_zoom = this.MaxZoom;
            
            this.YZoom = new_zoom;
        }
        
        public void ZoomIn(double xw, double yw)
        {
            double xmin_dist = xw - this.Plot.XMin;
            double xmax_dist = xw - this.Plot.XMax;
            double ymin_dist = yw - this.Plot.YMin;
            double ymax_dist = yw - this.Plot.YMax;
            this.SetXYMinMax(xw - xmin_dist/1.2, 
                             xw - xmax_dist/1.2,
                             yw - ymin_dist/1.2,
                             yw - ymax_dist/1.2);
        }
        
        public void ZoomOut()
        {
            double new_zoom = this.XZoom * 0.8;
            
            if (new_zoom < 1.0)
                new_zoom = 1.0;
            
            this.XZoom = new_zoom;
            
            new_zoom = this.YZoom * 0.8f;
            
            if (new_zoom < 1f)
                new_zoom = 1f;
            
            this.YZoom = new_zoom;
        }

        private void OnHChanged(Zoombar.Zoombar zb, bool released)
        {                        
        }
        
        private void OnHAsyncChanged(Zoombar.Zoombar zb, bool released)
        {
            if (released == false &&
                zb.Zoom > this.current_x_zoom &&
                this.Plot.DisableSelections == false)
            {
                this.Plot.GetXMinMax(zb.Pan, zb.Zoom, out double min, out double max);
                int physical_min = this.Plot.GetPhysicalXCoord(min);
                int physical_max = this.Plot.GetPhysicalXCoord(max);
                this.Plot.HSelection = new HorizontalSelection(this.Plot,
                                                               physical_min,
                                                               physical_max);
                this.Plot.AsyncRefresh(true);
            }
            else
            {
                this.Plot.SetXAxis(zb.Pan, zb.Zoom);
                this.current_x_zoom  = zb.Zoom;
                this.current_x_pan   = zb.Pan;
                this.Plot.HSelection = null;
                this.Plot.AsyncRefresh();
            }
        }
        
        private void OnHCallbackInvoked(Zoombar.Zoombar zb)
        {
        }
        
        private void OnVChanged(Zoombar.Zoombar zb, bool released)
        {
        }
        
        private void OnVAsyncChanged(Zoombar.Zoombar zb, bool released)
        {
            if (released == false &&
                zb.Zoom > this.current_y_zoom &&
                this.Plot.DisableSelections == false)
            {
                this.Plot.GetYMinMax(zb.Pan, zb.Zoom, out double min, out double max);
                int physical_min = this.Plot.GetPhysicalYCoord(min);
                int physical_max = this.Plot.GetPhysicalYCoord(max);
                this.Plot.VSelection = new VerticalSelection(this.Plot, physical_min, physical_max);
                this.Plot.AsyncRefresh(true);
            }
            else
            {
                this.Plot.SetYAxis(zb.Pan, zb.Zoom);
                this.current_y_zoom = zb.Zoom;
                this.current_y_pan  = zb.Pan;
                this.Plot.VSelection = null;
                this.Plot.AsyncRefresh();
            }
        }
        
        private void OnVCallbackInvoked(Zoombar.Zoombar zb)
        {
        }
        
        private void OnDataSetAdded(object o, EventArgs e)
        {
            this.Plot.SetXAxis(this.current_x_pan, this.current_x_zoom);
            this.Plot.SetYAxis(this.current_y_pan, this.current_y_zoom);
        }
        
        private void OnXAxesReversed(object o, EventArgs e)
        {
            this.XAxesReversed?.Invoke(o as IPlotEngine);
        }
        
        protected override bool OnButtonPressEvent(Gdk.EventButton evnt)
        {
            if (this.Plot.IsEmpty || this.Plot.IsValid == false)
                return base.OnButtonPressEvent(evnt);

            try
            {
                switch (evnt.Button)
                {
                
                case 1:
                    if (evnt.Type == Gdk.EventType.TwoButtonPress)
                    {
                        // double click
                        this.Plot.FitView();
                        this.XZoom = 1.0;
                        this.YZoom = 1.0;
                    }
                    else
                    {                
                        // single click
                        this.button1_drag_info = new DragInfo(this, evnt);
                        foreach (Annotation a in this.Plot.Annotations)
                        {
                            if (a.Interactive)
                            {
                                a.ButtonPressed((int)evnt.X, (int)evnt.Y, evnt.State);            
                                this.Plot.NotifyPlotChanges();
                            }
                        }
                    }
                    break;
                    
                case 2:
                    this.button2_drag_info = new DragInfo(this, evnt);
                    this.Plot.IsDragging = true;
                    break;
                
                case 3:
                    if (this.HasPopupDialog)
                        MainWindow.UI.EditMenu.Popup();
                    break;
                }
            }
            catch (NullReferenceException)
            {
                // plot may be in undefinded state
                Console.WriteLine("NullReferenceException in ScrolledPlot.OnButtonPressEvent()");
            }

            return base.OnButtonPressEvent(evnt);
        }
        
        protected override bool OnMotionNotifyEvent(Gdk.EventMotion evnt)
        {
            bool interactive_annotation = false;
            
            foreach (Annotation a in this.Plot.Annotations)
            {
                if (a.Interactive == false)
                    continue;
                    
                interactive_annotation = true;

                a.Motion((int)evnt.X, (int)evnt.Y, evnt.State);
                this.Plot.AsyncRefresh(true);
            }
            
            if (interactive_annotation == false)
            {
                if (this.Plot.Selection == null && this.button1_drag_info != null)
                {
                    this.Plot.SetSelection(this.button1_drag_info.PhysicalXStart,
                                           this.button1_drag_info.PhysicalYStart,
                                           0,
                                           0);
                }
                
                if (this.Plot.Selection != null)
                {
                    this.Plot.Selection.SetSize((int)evnt.X-this.Plot.Selection.XStart,
                                                (int)evnt.Y-this.Plot.Selection.YStart);
                }
                else if (this.button2_drag_info != null)
                {
                    this.button2_drag_info.ShowPan(evnt);
                }
            }

            this.Plot.GtkWidget.GdkWindow.GetOrigin(out int x, out int y);
            this.Plot.GtkWidget.GdkWindow.GetSize(out int w, out int h);

            if (this.PointerCoordsChanged != null &&
                (int)evnt.XRoot >= x && (int)evnt.XRoot <= x+w &&
                (int)evnt.YRoot >= y && (int)evnt.YRoot <= y+h)
            {
                try
                {
                    double xw = this.Plot.GetWorldXCoord((int)evnt.X);
                    double yw = this.Plot.GetWorldYCoord((int)evnt.Y);

                    this.PointerCoordsChanged?.Invoke(xw, yw);
                }
#pragma warning disable RECS0022
                catch { }
#pragma warning restore RECS0022
            }

            return base.OnMotionNotifyEvent(evnt);
        }

        protected override bool OnButtonReleaseEvent(Gdk.EventButton evnt)
        {
            switch (evnt.Button)
            {
                
            case 1:
                this.button1_drag_info = null;
                foreach (Annotation a in this.Plot.Annotations)
                {
                    if (a.Interactive)
                    {
                        a.ButtonReleased((int)evnt.X, (int)evnt.Y, evnt.State);            
                        this.Plot.NotifyPlotChanges();
                        this.Plot.AsyncRefresh(true);
                    }
                }
                if (this.Plot.Selection == null)
                    break;
                if (this.Plot.Selection.Width  > MIN_SELECTION_LENGTH || 
                    this.Plot.Selection.Height > MIN_SELECTION_LENGTH)
                {
                    double x_min = this.Plot.GetWorldXCoord(this.Plot.Selection.X);
                    double x_max = this.Plot.GetWorldXCoord(this.Plot.Selection.X +
                                                            this.Plot.Selection.Width);
                    double y_max = this.Plot.GetWorldYCoord(this.Plot.Selection.Y);
                    double y_min = this.Plot.GetWorldYCoord(this.Plot.Selection.Y + 
                                                            this.Plot.Selection.Height);
                    
                    if (this.Plot.ReverseXAxes)
                        this.SetXYMinMax(x_max, x_min, y_min, y_max);
                    else
                        this.SetXYMinMax(x_min, x_max, y_min, y_max);
                }
                this.Plot.Selection = null;
                this.Plot.Refresh();
                break;
                
            case 2:
                if (this.button2_drag_info == null)
                    break;
                this.button2_drag_info.ApplyPan();
                this.button2_drag_info = null;
                this.Plot.IsDragging = false;
                break;

            }
            
            return base.OnButtonReleaseEvent(evnt);
        }
        
        protected override bool OnScrollEvent(Gdk.EventScroll evnt)
        {
            if (this.Plot.IsEmpty || this.Plot.IsValid == false)
                return base.OnScrollEvent(evnt);

            try
            {
                this.Plot.GtkWidget.GdkWindow.GetSize(out int w, out int h);
                this.Plot.GtkWidget.GdkWindow.GetPointer(out int x,
                                                         out int y,
                                                         out Gdk.ModifierType modifier);
                
                if (x <= w && y <= h)
                {
                    bool zoomed = true;
                    
                    switch (evnt.Direction)
                    {

                    case Gdk.ScrollDirection.Up:
                        double xw = this.Plot.GetWorldXCoord((int)evnt.X);
                        double yw = this.Plot.GetWorldYCoord((int)evnt.Y);
                        this.ZoomIn(xw, yw);
                        break;
                        
                    case Gdk.ScrollDirection.Down:
                        this.ZoomOut();
                        break;
                        
                    default:
                        zoomed = false;
                        break;
                    
                    }
                    
                    if (zoomed)
                    {
                        try
                        {
                            double xw = this.Plot.GetWorldXCoord((int)evnt.X);
                            double yw = this.Plot.GetWorldYCoord((int)evnt.Y);

                            this.PointerCoordsChanged?.Invoke(xw, yw);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"Exception while scroll event: {ex.ToString()}");
                        }
                    }
                }
            }
            catch (NullReferenceException)
            {
                // plot may be in undefined state
                Console.WriteLine("NullReferenceException in ScrolledPlot.OnScrollEvent()");
            }

            return base.OnScrollEvent(evnt);
        }

        public double MaxZoom
        {
            get
            {
                return this.HZoombar.MaxZoom;
            }
            set
            {
                this.HZoombar.MaxZoom = value;
                this.VZoombar.MaxZoom = value;
                this.Plot.AsyncRefresh();
            }
        }

        public Zoombar.ZoomType ZoomType
        {
            get
            {
                return this.HZoombar.ZoomType;
            }
            set
            {
                this.HZoombar.ZoomType = value;
                this.VZoombar.ZoomType = value;
                this.Plot.AsyncRefresh();
            }
        }

        public double XZoom
        {
            get { return this.HZoombar.Zoom;  }
            set { this.HZoombar.Zoom = value; }
        }
        
        public double XPan
        {
            get { return this.HZoombar.Pan;  }
            set { this.HZoombar.Pan = value; }
        }
        
        public double YZoom
        {
            get { return this.VZoombar.Zoom;  }
            set { this.VZoombar.Zoom = value; }
        }
        
        public double YPan
        {
            get { return this.VZoombar.Pan;  }
            set { this.VZoombar.Pan = value; }
        }

        public IPlotEngine Plot { get; }

        public double XMin
        {
            get
            {
                return this.Plot.XMin; 
            }
            set 
            { 
                double xmin = value;                
                double xmax = this.Plot.XMax;
                double gxmn = this.Plot.GlobalXMin;
                double gxmx = this.Plot.GlobalXMax;
                
                double global_range = gxmx - gxmn;
                double new_range    = xmax - xmin;
                
                if (global_range/new_range > this.HZoombar.MaxZoom || xmin >= xmax)
                    xmin = this.Plot.XMax - global_range/this.HZoombar.MaxZoom;
                
                double pan, zoom;
                if ((gxmn - gxmx + xmax - xmin) == 0.0)
                    pan = 0.5;
                else
                    pan  = (gxmn - xmin) / (gxmn - gxmx + xmax - xmin);
                zoom = (gxmx - gxmn) / (xmax -xmin);
                
                if (this.Plot.ReverseXAxes)
                    pan = 1.0 - pan;
                
                this.HZoombar.SetPanZoom(pan, zoom);
            }
        }
        
        public void SetXYMinMax(double x_min, double x_max, double y_min, double y_max, bool expand_range)
        {
            if (expand_range)
            {
                if (x_min < this.Plot.GlobalXMin)
                    this.Plot.SetGlobalXMinMax(x_min, this.Plot.GlobalXMax);
                if (x_max > this.Plot.GlobalXMax)
                    this.Plot.SetGlobalXMinMax(this.Plot.GlobalXMin, x_max);
                if (y_min < this.Plot.GlobalYMin)
                    this.Plot.SetGlobalYMinMax(y_min, this.Plot.GlobalYMax);
                if (y_max > this.Plot.GlobalYMax)
                    this.Plot.SetGlobalYMinMax(this.Plot.GlobalYMin, y_max);
            }

            this.SetXYMinMax(x_min, x_max, y_min, y_max);
        }
        
        public void SetXYMinMax(double x_min, double x_max, double y_min, double y_max)
        {
            double xmin = x_min;
            double xmax = x_max;
            double gxmn = this.Plot.ExtendedGlobalXMin;
            double gxmx = this.Plot.ExtendedGlobalXMax;
            
            double global_range = gxmx - gxmn;
            double new_range    = xmax - xmin;
                
            if (global_range/new_range > this.HZoombar.MaxZoom || xmin >= xmax)
                xmin = x_max - global_range/this.HZoombar.MaxZoom;
            
            double pan, zoom;
            if (Math.Abs((gxmn - gxmx + xmax - xmin)) < EPSILON)
                pan = 0.5;
            else
                pan  = (gxmn - xmin) / (gxmn - gxmx + xmax - xmin);
            zoom = (gxmx - gxmn) / (xmax -xmin);
            
            if (this.Plot.ReverseXAxes)
                pan = 1.0 - pan;
            
            this.HZoombar.SetPanZoom(pan, zoom);
            
            
            double ymin = y_min;
            double ymax = y_max;
            double gymn = this.Plot.ExtendedGlobalYMin;
            double gymx = this.Plot.ExtendedGlobalYMax;
            
            global_range = gymx - gymn;
            new_range    = ymax - ymin;
            
            if (global_range/new_range > this.VZoombar.MaxZoom || ymin >= ymax)
                ymin = y_max - global_range/this.VZoombar.MaxZoom;
            
            if (Math.Abs((gymx - gymn - ymax + ymin)) < EPSILON)
                pan = 0.5;
            else
                pan  = (gymx - ymax) / (global_range - ymax + ymin);
            zoom = global_range / (ymax -ymin);
            
            this.VZoombar.SetPanZoom(pan, zoom);
        }
        
        public List<MetaData> CreateMetaData()
        {
            List<MetaData> meta_data = new List<MetaData>
            {
                new MetaData("Title", MetaData.SerializeString(this.plot_info.Name))
            };

            foreach (Annotation a in this.Plot.Annotations)
            {
                meta_data.Add(new MetaData("Annotation", a.Serialize()));
            }
            
            return meta_data;
        }
        
        public void RestoreAnnotations(List<MetaData> meta_data)
        {
            if (meta_data == null)
                return;
            
            foreach (MetaData md in meta_data)
            {
                if (md.Type != "Annotation")
                    continue;
                
                Annotation a = Annotation.Deserialize(this.Plot, md.Data);
                
                if (!this.Plot.Annotations.Contains(a))
                    this.Plot.Annotations.Add(a);
            }
        }
                
        public double XMax
        {
            get
            {
                return this.Plot.XMax;
            }
            set
            {
                double xmin = this.Plot.XMin;
                double xmax = value;
                double gxmn = this.Plot.ExtendedGlobalXMin;
                double gxmx = this.Plot.ExtendedGlobalXMax;
                
                double global_range = gxmx - gxmn;
                double new_range    = xmax - xmin;
                
                if (global_range/new_range > this.HZoombar.MaxZoom || xmin >= xmax)
                    xmax = this.Plot.XMin + global_range/this.HZoombar.MaxZoom;
                
                double pan, zoom;
                if (Math.Abs((gxmn - gxmx + xmax - xmin)) < EPSILON)
                    pan = 0.5;
                else
                    pan  = (gxmn - xmin) / (gxmn - gxmx + xmax - xmin);
                zoom = (gxmx - gxmn) / (xmax -xmin);
                
                if (this.Plot.ReverseXAxes)
                    pan = 1.0 - pan;

                this.HZoombar.SetPanZoom(pan, zoom);
            }
        }
                
        public double YMin
        {
            get
            {
                return this.Plot.YMin;
            }
            set
            {
                if (value < this.Plot.GlobalYMin)
                    this.Plot.SetGlobalYMinMax(value, this.Plot.GlobalYMax);
                    
                double ymin = value;
                double ymax = this.Plot.YMax;
                double gymn = this.Plot.ExtendedGlobalYMin;
                double gymx = this.Plot.ExtendedGlobalYMax;

                double global_range = gymx - gymn;
                double new_range    = ymax - ymin;
                
                if (global_range/new_range > this.VZoombar.MaxZoom || ymin >= ymax)
                    ymin = this.Plot.YMax - global_range/this.VZoombar.MaxZoom;

                double pan, zoom;
                if (Math.Abs((gymx - gymn - ymax + ymin)) < EPSILON)
                    pan = 0.5;
                else
                    pan  = (gymx - ymax) / (global_range - ymax + ymin);
                zoom = global_range / (ymax -ymin);
                
                this.VZoombar.SetPanZoom(pan, zoom);
            }
        }
                
        public double YMax
        {
            get
            {
                return this.Plot.YMax;
            }
            set
            {
                if (value > this.Plot.GlobalYMax)
                    this.Plot.SetGlobalYMinMax(this.Plot.GlobalYMin, value);
                
                double ymin = this.Plot.YMin;
                double ymax = value;
                double gymn = this.Plot.ExtendedGlobalYMin;
                double gymx = this.Plot.ExtendedGlobalYMax;
                
                double global_range = gymx - gymn;
                double new_range    = ymax - ymin;
                
                if (global_range/new_range > this.VZoombar.MaxZoom || ymin >= ymax)
                    ymax = this.Plot.YMin + global_range/this.VZoombar.MaxZoom;
                
                double pan, zoom;
                if (Math.Abs((gymx - gymn - ymax + ymin)) < EPSILON)
                    pan = 0.5;
                else
                    pan  = (gymx - ymax) / (gymx - gymn - ymax + ymin);
                zoom = (gymx - gymn) / (ymax -ymin);
                
                this.VZoombar.SetPanZoom(pan, zoom);
            }
        }        
        
        public new bool Sensitive
        {
            get 
            { 
                return this.VZoombar.Sensitive && this.HZoombar.Sensitive;
            }
            set
            {
                this.HZoombar.Sensitive = value;
                this.VZoombar.Sensitive = value;
            }
        }

        public bool HasPopupDialog { get; set; }

        public HZoombar HZoombar { get; private set; }
        public VZoombar VZoombar { get; private set; }
    }
}