// ZoomBar.cs created with MonoDevelop
// User: klose at 13:48 16.12.2008
// CVS release: $Id: ZoomBar.cs,v 1.14 2010-11-25 13:52:56 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;


namespace Hiperscan.QuickStep.Zoombar
{

    public struct Defaults
    {
        public const double HandleLength = 28;
        public const double MinBarLength = 20;
        public const double ButtonLength = 20;
    }
    
    public enum ZoomType
    {
        Linear,
        Quadratic,
        Cubic,
        Proportional
    }
    
    public class Zoombar : Gtk.Alignment
    {
        public const double EPSILON = 1e-30f;

        internal List<ZoomBarButton> buttons;
        
        protected Frame frame;
        private readonly Fixed fix;
        
        private readonly bool is_horizontal;
        
        protected double zoom = 1.0;
        protected double pan  = 0.5;
        protected double scroll_step = 0.05;
        private   double max_zoom  = 300;
        private   double last_zoom = 1.0;
        private   double last_pan  = 0.0;
        
        private ZoomType zoom_type = ZoomType.Proportional;

        protected bool dragging = false;
        private   int  drag_xoffset, drag_yoffset;
        
        protected int frame_width, frame_height;
        protected int width = 19;
        protected double lmin, lmax, l, w, offset;
        
        public delegate void ChangedHandler(Zoombar zb, bool released);
        public event         ChangedHandler Changed;

        public delegate void AsyncChangedHandler(Zoombar zb, bool released);
        public event         AsyncChangedHandler AsyncChanged;
        
        public delegate void AsyncChangedCallbackHandler(Zoombar zb);
        public event         AsyncChangedCallbackHandler AsyncChangedCallbackInvoked;

        
        protected Zoombar(float xalign, float yalign, float xscale, float yscale)
            : base(xalign, yalign, xscale, yscale)
        {
            if (Math.Abs(xscale - 1f) < EPSILON)
                this.is_horizontal = true;
            else
                this.is_horizontal = false;

            // initialize frame
            this.frame = new Frame
            {
                BorderWidth = 0,
                ShadowType = ShadowType.In
            };

            // initialize fixed
            this.fix = new Fixed
            {
                HeightRequest = this.width,
                BorderWidth = 0,
                HasWindow = false
            };
            this.fix.SizeAllocated += OnFixedSizeAllocated;                                    

            // initialize handles, slides, buttons
            this.buttons = new List<ZoomBarButton>();
            for (int ix = 0; ix < 7; ++ix)
            {
                if (ix < 2)
                {
                    Slide slide    = new Slide();
                    slide.Pressed += this.OnSlidePressed;
                    this.buttons.Add(slide);
                }
                else if (ix < 5)
                {
                    Handle handle  = new Handle();
                    handle.Events |= Gdk.EventMask.PointerMotionMask;
                    handle.Pressed       += this.OnHandlePressed;
                    handle.Released      += this.OnHandleReleased;
                    handle.DoubleClicked += this.OnDoubleClicked;
                    this.buttons.Add(handle);
                }
                else
                {
                    ScrollButton button = new ScrollButton();
                    button.Pressed += this.OnButtonPress;
                    this.buttons.Add(button);
                }
                this.buttons[ix].CanFocus     = false;
                this.buttons[ix].UseUnderline = false;
                this.fix.Add(this.buttons[ix]);
            }
            this.buttons[2].MotionNotifyEvent += this.OnCenterHandleMotion;
            this.buttons[3].MotionNotifyEvent += this.OnLeftUpHandleMotion;
            this.buttons[4].MotionNotifyEvent += this.OnRightDownHandleMotion;
            
            // pack widgets
            this.frame.Add(this.fix);
            this.Add(this.frame);
        }
        
        protected void Move(Gtk.Widget w, int x, int y)
        {
            this.fix.Move(w, x, y);
        }

        protected new void SetSizeRequest(int x, int y)
        {
            this.fix.SetSizeRequest(x, y);
        }

        protected double NewLength(double new_zoom)
        {
            switch (zoom_type)
            {
                
            case ZoomType.Linear:
                double a = (lmin - lmax) / (max_zoom - 1.0);
                double b = lmax - a;
                return a * new_zoom + b;
                                
            case ZoomType.Quadratic:
                return lmax - Math.Sqrt( Math.Pow(lmax-lmin, 2) * (new_zoom-1.0) * (max_zoom-1.0) ) / (max_zoom-1.0); 
                
            case ZoomType.Cubic:
                return lmax - Math.Pow( Math.Pow(lmax-lmin, 3) * (new_zoom-1.0) * Math.Pow(max_zoom-1.0, 2), 1/3.0 ) / (max_zoom-1.0);
                
            case ZoomType.Proportional:
                return (lmax-2*Defaults.HandleLength - Defaults.MinBarLength) / new_zoom + 2*Defaults.HandleLength + Defaults.MinBarLength;
            
            default:
                throw new NotSupportedException("Unknown ZoomType.");
                
            }
        }
        
        protected double NewZoom(double new_l)
        {
            switch (zoom_type)
            {
                
            case ZoomType.Linear:
                double a = (lmin - lmax) / (max_zoom - 1.0);
                double b = lmax - a;
                return (new_l - b) / a;

            case ZoomType.Quadratic:
                return (max_zoom - 1.0) * Math.Pow( (lmax - new_l) / (lmax - lmin) , 2 ) + 1.0; 
                
            case ZoomType.Cubic:
                return (max_zoom - 1.0) * Math.Pow( (lmax - new_l) / (lmax - lmin) , 3 ) + 1.0; 
                
            case ZoomType.Proportional:
                return (lmax-2*Defaults.HandleLength-Defaults.MinBarLength) / (new_l-2*Defaults.HandleLength-Defaults.MinBarLength); 
                
            default:
                throw new NotSupportedException("Unknown ZoomType.");
                
            }
        }

        protected void CalculateGeomParams(int new_length, int new_width)
        {            
            lmin   = 2 * Defaults.HandleLength + Defaults.MinBarLength;
            lmax   = (double)new_length - 2*Defaults.ButtonLength;
            l      = this.NewLength(zoom);
            w      = (double)new_width;
            offset = pan * (lmax - l);
        }
        
        public void SetPanZoom(double pan, double zoom)
        {
            if (pan < 0.0) pan = 0.0;
            if (pan > 1.0) pan = 1.0;
            if (zoom < 1.0) zoom = 1.0;
            if (zoom > this.max_zoom) zoom = this.max_zoom;
            
            this.pan  = pan;
            this.zoom = zoom;
            
            this.Update();
        }
        
        virtual protected void Update()
        {
            throw new MissingMethodException("The method Update() must be implemented in classes derived from Zoombar.");
        }
        
        public void ChangeNotify(bool released)
        {
            if (this.AsyncChanged != null)
            {
                foreach (AsyncChangedHandler del in this.AsyncChanged.GetInvocationList())
                {
                    del.BeginInvoke(this, released, new AsyncCallback(AsyncChangedCallback), del);
                }
            }

            this.Changed?.Invoke(this, released);
        }
        
        private void AsyncChangedCallback(IAsyncResult iar)
        {
            this.AsyncChangedCallbackInvoked?.Invoke(this);
        }

        private void OnButtonPress(object o, EventArgs e)
        {
            Button b = o as Button;
            
            double pan_step = scroll_step/(zoom-1.0);

            if (b == this.buttons[6])
            {
                if (pan + pan_step <= 1.0)
                    pan += pan_step;
                else
                    pan = 1.0;
            }
            else
            {
                if (pan - pan_step >= 0.0)
                    pan -= pan_step;
                else
                    pan = 0.0;
            }            
            this.Update();
        }        
        
        private void OnSlidePressed(object o, System.EventArgs e)
        {
            Slide s = o as Slide;
            
            double pan_step = 0.9/(zoom-1.0);

            if (s == this.buttons[1])
            {
                if (pan + pan_step <= 1.0)
                    pan += pan_step;
                else
                    pan = 1.0;
            }
            else
            {
                if (pan - pan_step >= 0.0)
                    pan -= pan_step;
                else
                    pan = 0.0;
            }
            this.Update();
        }

        protected void OnHandlePressed(object o, System.EventArgs e)
        {
            Handle h = o as Handle;
            h.Parent.GetPointer(out int x, out int y);
            dragging = true;
            drag_xoffset = x;
            drag_yoffset = y;
        }

        protected void OnHandleReleased(object o, System.EventArgs e)
        {
            dragging = false;
            this.ChangeNotify(true);
        }

        private void OnDoubleClicked(Handle o)
        {
            if (Math.Abs(pan - 0.5) > EPSILON && Math.Abs(zoom - 1.0) > EPSILON)
            {
                last_pan  = pan;
                last_zoom = zoom;
                pan       = 0.5;
                zoom      = 1.0;
            }
            else
            {
                zoom = last_zoom;
                pan  = last_pan;
            }    
            this.Update();
        }
        
        private void OnLeftUpHandleMotion(object o, MotionNotifyEventArgs args)
        {
            Handle h = o as Handle;
            int x, y;
            h.Parent.GetPointer(out x, out y);

            if (dragging)
            {
                double drag;
                
                if (this.is_horizontal)
                    drag = x - drag_xoffset;
                else
                    drag = y - drag_yoffset;
                
                drag_xoffset = x;
                drag_yoffset = y;

                double new_zoom, new_pan, new_l;
                
                if ((h.ModifierState & Gdk.ModifierType.ControlMask) != Gdk.ModifierType.None)
                    new_l = l - drag*2;
                else
                    new_l = l - drag;
                
                new_zoom = this.NewZoom(new_l);
                new_pan  = (offset + drag) / (lmax - new_l);
                
                if (new_zoom >= 1.0 && 
                    new_zoom <= max_zoom &&
                    new_pan  >= 0.0 &&
                    new_pan  <= 1.0)
                {
                    zoom = new_zoom;
                    pan  = new_pan;
                    this.Update();
                }
            }    
        }

        private void OnCenterHandleMotion(object o, MotionNotifyEventArgs args)
        {
            Handle h = o as Handle;
            h.Parent.GetPointer(out int x, out int y);

            if (dragging)
            {
                double drag;
                
                if (this.is_horizontal)
                    drag = x - drag_xoffset;
                else
                    drag = y - drag_yoffset;
                
                drag_xoffset = x;
                drag_yoffset = y;
                
                double new_pan  = (offset + drag) / (lmax - l);

                if (new_pan  >= 0.0 &&
                    new_pan  <= 1.0)
                {
                    pan = new_pan;
                }
                else
                {
                    pan = Math.Round(pan);
                }
                this.Update();
            }
        }
        
        private void OnRightDownHandleMotion(object o, MotionNotifyEventArgs e)
        {
            Handle h = o as Handle;
            h.Parent.GetPointer(out int x, out int y);

            if (dragging)
            {
                double drag;
                
                if (this.is_horizontal)
                    drag = x - drag_xoffset;
                else
                    drag = y - drag_yoffset;
                
                drag_xoffset = x;
                drag_yoffset = y;

                double new_zoom, new_l, new_pan;

                if ((h.ModifierState & Gdk.ModifierType.ControlMask) != Gdk.ModifierType.None)
                {
                    new_zoom = this.NewZoom(l + 2*drag);
                    new_pan  = (offset - drag) / (lmax - l - 2*drag);
                }
                else
                {
                    new_l    = l + drag;
                    new_zoom = this.NewZoom(new_l);
                    new_pan  = offset / (lmax - new_l);
                }

                if (new_zoom >= 1.0 && 
                    new_zoom <= max_zoom &&
                    new_pan  >= 0.0 &&
                    new_pan  <= 1.0)
                {
                    zoom = new_zoom;
                    pan  = new_pan;
                    this.Update();
                }
            }                        
        }

        private void OnFixedSizeAllocated(object o, SizeAllocatedArgs e)
        {
            if (frame_width  != e.Allocation.Width ||
                frame_height != e.Allocation.Height)
            {
                frame_width  = e.Allocation.Width;
                frame_height = e.Allocation.Height;
                this.Update();
            }
        }

        public double Zoom
        {
            get
            {
                return this.zoom;
            }
            set
            {
                this.zoom = value;
                this.Update();
            }
        }
        
        public ZoomType ZoomType
        {
            get
            {
                return this.zoom_type;
            }
            set
            {
                this.zoom_type = value;
                this.Update();
            }
        }

        public double Pan
        {
            get
            {
                return this.pan;
            }
            set
            {
                this.pan = value;
                this.Update();
            }
        }

        public double MaxZoom
        {
            get
            {
                return max_zoom;
            }
            set
            {
                max_zoom = value;
                this.Update();
            }
        }
        
        public double ScrollStep
        {
            get
            {
                return scroll_step;
            }
            set
            {
                scroll_step = value;
            }
        }

        public ShadowType ShadowType
        {
            get
            {
                return this.frame.ShadowType;
            }
            set 
            {
                this.frame.ShadowType = value;
                this.Update();
            }
        }
        
        public new uint BorderWidth
        {
            get
            {
                return this.frame.BorderWidth;
            }
            set
            {
                this.frame.BorderWidth = value;
                this.Update();
            }
        }
        
        public int Width
        {
            get
            {
                return this.width;
            }
            set
            {
                this.width = value;
                this.fix.WidthRequest = value;
                this.Update();
            }
        }

        public bool IsDragging => this.dragging;
    }
}
