// Created with MonoDevelop
//
//    Copyright (C) 2010 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 System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Threading;

using Gtk;
using Hiperscan.NPlotEngine.Overlay;
using Hiperscan.NPlotEngine.Overlay.Annotation;
using Hiperscan.NPlotEngine.Overlay.Selection;
using Hiperscan.Spectroscopy;

using NPlot;


namespace Hiperscan.NPlotEngine
{

    public partial class NPlotEngine : Hiperscan.NPlotEngine.IPlotEngine, IDisposable
    {
        
        private const double EPSILON = 1e-50;

        private PlotSurface2D plotsurface;
        private Grid grid;
        private Legend legend;
        private LinearAxis xaxis1, yaxis1;

        private readonly LinearAxis xaxis2;
        private readonly LinearAxis yaxis2;

        private readonly double x_min_range = 0.001;

        private volatile bool is_empty    = true;
        private volatile bool is_dragging = false;
        private volatile bool disable_selections = false;
        private volatile bool created = false;
        private volatile bool use_second_axes = false;
        
        private readonly PlotEngineType type = PlotEngineType.Normal;
        
        private volatile int plot_type_id;
        
        private readonly string xlabel = "x";
        private readonly string ylabel = "y";
        
        private RubberBandSelection selection;
        private HorizontalSelection horizontal_selection;
        private VerticalSelection   vertical_selection;
        private readonly List<CrossHair> markers;
        private readonly List<Annotation> annotations;
        private readonly List<DataSetSelection> data_set_selections;
        
        private volatile bool lock_axes = false;
        private volatile bool data_sets_changed  = false;
        private volatile bool drawable_changed   = false;
        private volatile bool refresh_is_pending = false;
        private volatile bool refresh_overlay_is_pending = false;
        private volatile bool xaxis_changed, yaxis_changed;
        private volatile bool plot_changed   = false;
        private volatile bool legend_visible = true;
        private volatile bool reverse_x_axes = false;
        
        public event EventHandler<EventArgs> Refreshed;
        public event EventHandler<EventArgs> DataSetAdded;
        public event EventHandler<EventArgs> DataSetRemoved;
        public event EventHandler<EventArgs> DataSetsChanged;
        public event EventHandler<EventArgs> PlotChanged;
        public event EventHandler<EventArgs> PlotCreated;
        public event EventHandler<EventArgs> AutoRemoveLegend;
        public event EventHandler<EventArgs> XAxesReversed;
        
        
        public NPlotEngine(PlotEngineType type)
        {
            this.type = type;

            this.plotsurface = new PlotSurface2D();
            this.__drawables = new DrawableInfoList(); 

            this.__removed_drawables  = Queue.Synchronized(new Queue());
            this.__added_drawables    = Queue.Synchronized(new Queue());
            this.__added_bg_drawables = Queue.Synchronized(new Queue());
            this.__data_sets            = new Dictionary<string,DataSet>();

            this.__background_data_sets = new Dictionary<string,DataSet>();
            this.selection   = null;
            this.markers     = new List<CrossHair>();
            this.annotations = new List<Annotation>();
            this.data_set_selections = new List<DataSetSelection>();
            
            this.plotsurface.Events |= Gdk.EventMask.ButtonPressMask;
            this.plotsurface.Events |= Gdk.EventMask.ButtonReleaseMask;
            this.plotsurface.Events |= Gdk.EventMask.PointerMotionMask;
            
            this.GlobalXMin = double.PositiveInfinity;
            this.GlobalYMin = double.PositiveInfinity;
            this.GlobalXMax = double.NegativeInfinity;
            this.GlobalYMax = double.NegativeInfinity;
            
            this.plotsurface.AutoScaleAutoGeneratedAxes = false;
            this.plotsurface.AutoScaleTitle = false;
            this.plotsurface.Legend = null;

            this.plotsurface.XAxis1 = null;
            this.plotsurface.XAxis2 = null;
            this.plotsurface.YAxis1 = null;
            this.plotsurface.YAxis2 = null;
            
            this.plotsurface.SmoothingMode = SmoothingMode.AntiAlias;

            // grid
            this.grid = new Grid
            {
                VerticalGridType = Grid.GridType.Fine,
                HorizontalGridType = Grid.GridType.Fine
            };

            Pen pen = new Pen(Color.Black);
            float[] pattern = { 1.0f, 2.0f };
            pen.DashPattern = pattern;
            this.grid.MajorGridPen = pen;

            // axes
            this.xaxis1 = new LinearAxis(this.GlobalXMin, this.GlobalXMax)
            {
                AxisColor     = Color.Black,
                LabelColor    = Color.Black,
                TickTextColor = Color.Black,
                TicksAngle    = (float)(1.5f * Math.PI),
                Label         = this.xlabel
            };
            this.plotsurface.XAxis1 = new LinearAxis(this.xaxis1);

            this.xaxis2 = new LinearAxis(this.ExtendedGlobalXMin, this.ExtendedGlobalXMax)
            {
                AxisColor     = Color.Black,
                LabelColor    = Color.Black,
                TickTextColor = Color.Black,
                Label         = this.xlabel
            };
            this.plotsurface.XAxis2 = new LinearAxis(this.xaxis2);

            this.yaxis1 = new LinearAxis(this.ExtendedGlobalYMin, this.ExtendedGlobalYMax)
            {
                AxisColor     = Color.Black,
                LabelColor    = Color.Black,
                TickTextColor = Color.Black,
                Label         = this.ylabel
            };
            this.plotsurface.YAxis1 = new LinearAxis(this.yaxis1);

            this.yaxis2 = new LinearAxis(this.ExtendedGlobalYMin, this.ExtendedGlobalYMax)
            {
                AxisColor     = Color.Black,
                LabelColor    = Color.Black,
                TickTextColor = Color.Black,
                Label         = this.ylabel,
                TicksAngle    = (float)(1.5f * Math.PI)
            };
            this.plotsurface.YAxis2 = new LinearAxis(this.yaxis2);
            
            // legend
            this.legend = new Legend();
            this.legend.AttachTo(NPlot.PlotSurface2D.XAxisPosition.Bottom, NPlot.PlotSurface2D.YAxisPosition.Right);
            this.legend.VerticalEdgePlacement   = Legend.Placement.Inside;
            this.legend.HorizontalEdgePlacement = Legend.Placement.Inside;
            this.legend.XOffset = -9;
            this.legend.YOffset = -9;
            this.legend.BackgroundColor = Color.FromArgb(190, Color.White);
            
            this.plotsurface.LegendZOrder = int.MaxValue;
            
            this.plotsurface.AddLegend += (o, e) =>
            {
                if (this.legend_visible)
                    this.plotsurface.Legend = this.legend;
            };
            
            this.plotsurface.AutoRemoveLegend += (sender, e) =>
            {
                if (this.AutoRemoveLegend != null)
                    Application.Invoke((_sender, _e) => this.AutoRemoveLegend(this, EventArgs.Empty));
            };
            
            this.plotsurface.LinesDrawn += this.DrawOverlays;
        
            switch (this.type)
            {
                
            case PlotEngineType.Normal:
                this.StartAutoUpdate();
                break;
                
            case PlotEngineType.Synchronous:
                break;
                
            case PlotEngineType.Preview:
                this.plotsurface.PlotBackColor = Color.Transparent;
                this.legend = null;
                break;
                
            }
            
            // thumbnail creation
            this.__thumbnail_bitmap = new Bitmap(thumbnail_width, thumbnail_height);
            this.__thumbnail_plotsurface = new PlotSurface2D
            {
                AutoScaleAutoGeneratedAxes = false,
                AutoScaleTitle = false,
                SmoothingMode = SmoothingMode.None
            };
            this.__ResetThumbnail();
        }
        
        public NPlotEngine() : this(PlotEngineType.Normal)
        {
        }
        
        public void Dispose()
        {
            if (this.__thread_running)
                this.do_process_changes.Abort();
        }
        
        ~NPlotEngine()
        {
        }
        
        public NPlotEngine(NPlotEngine plot) : this(PlotEngineType.Preview)
        {
            this.type = PlotEngineType.Normal;
            
            this.DataSets = new Dictionary<string,DataSet>(plot.DataSets);
            
            this.legend = plot.legend;
            this.grid   = plot.grid;
            this.XMin   = plot.XMin;
            this.XMax   = plot.XMax;
            this.YMin   = plot.YMin;
            this.YMax   = plot.YMax;
            this.XLabel = plot.XLabel;
            this.YLabel = plot.YLabel;
            
            this.__ProcessChanges();
        }
        
        public void NotifyPlotChanges()
        {
            lock (this.plotsurface)
                this.plot_changed = true;
        }
        
        public void Clear()
        {
            if (this.is_empty == false)
            {
                lock (this)
                {
                    this.__data_sets.Clear();
                    this.__removed_drawables.Clear();
                    this.__added_drawables.Clear();
                    this.__added_bg_drawables.Clear();
                    this.__drawables.Clear();
                    this.is_empty = true;
                    this.plot_changed = true;
                    this.__ResetThumbnail();
                }

                this.DataSetRemoved?.Invoke(this, EventArgs.Empty);
                this.DataSetsChanged?.Invoke(this, EventArgs.Empty);
            }

            this.plotsurface.Clear();
            this.refresh_is_pending = true;
            
            this.plotsurface.XAxis1 = null;
            this.plotsurface.YAxis1 = null;
            this.plotsurface.XAxis2 = null;
            this.plotsurface.YAxis2 = null;

            this.GlobalXMin = double.PositiveInfinity;
            this.GlobalYMin = double.PositiveInfinity;
            this.GlobalXMax = double.NegativeInfinity;
            this.GlobalYMax = double.NegativeInfinity;
            
            if (this.GtkWidget.GdkWindow != null)
                this.GtkWidget.GdkWindow.Cursor = new Gdk.Cursor(Gdk.CursorType.Arrow);
        }
        
        public void ProcessChanges()
        {
            if (this.do_process_changes != null)
                return;
            
            this.data_sets_changed = true;
            this.__ProcessChanges();
        }

        public void Refresh()
        {
            this.Refresh(false);
        }

        public void Refresh(bool only_overlays)
        {
            try
            {
                this.plotsurface.UpdateCache(only_overlays);
                this.plotsurface.QueueDrawArea();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception in NPlotEngine.Refresh(): " + ex.Message);
            }

            this.Refreshed?.Invoke(this, EventArgs.Empty);
        }

        public void AddDataSet(DataSet ds)
        {
            lock (this)
            {
                List<string> current_ids = new List<string>(this.__data_sets.Keys);
                foreach (string id in current_ids)
                {
                    if (id == ds.Id)
                        this.__data_sets.Remove(id);
                }
                
                this.__data_sets.Add(ds.Id, ds);

                this.data_sets_changed = true;
            }

            this.DataSetAdded?.Invoke(this, EventArgs.Empty);
        }

        public void FitView()
        {
            bool success = false;
            
            lock (this)
            {
                foreach (DataSet ds in this.__data_sets.Values)
                {
                    if (ds.IsVisible == false)
                        continue;

                    if (success == false)
                    {
                        this.GlobalXMax = double.NegativeInfinity;
                        this.GlobalYMax = double.NegativeInfinity;
                        this.GlobalXMin = double.PositiveInfinity;
                        this.GlobalYMin = double.PositiveInfinity;
                        success = true;
                    }

                    this.SetGlobalMinMax(ds);
                }
            }
            
            if (this.__data_sets.Count > 0 && success == false)
            {
                Console.WriteLine("Cannot fit plot if all datasets are set invisible.");
                return;
            }
                
            this.XMin = this.ExtendedGlobalXMin;
            this.XMax = this.ExtendedGlobalXMax;
            this.YMin = this.ExtendedGlobalYMin;
            this.YMax = this.ExtendedGlobalYMax;
        }
        
        public void SetGlobalXMinMax(double x_min, double x_max)
        {
            if (this.GlobalXMin > x_min)
                this.GlobalXMin = x_min;
            
            if (this.GlobalXMax < x_max)
                this.GlobalXMax = x_max;            
        }
        
        public void SetGlobalXMinMax(double x_min, double x_max, bool force)
        {
            this.GlobalXMin = x_min;
            this.GlobalXMax = x_max;            
        }

        public void SetGlobalYMinMax(double y_min, double y_max)
        {
            if (this.GlobalYMin > y_min)
                this.GlobalYMin = y_min;
            
            if (this.GlobalYMax < y_max)
                this.GlobalYMax = y_max;
        }

        private void SetGlobalMinMax(DataSet ds)
        {
            if (ds.IsBackground)
                return;
            
            double x_min, x_max, y_min, y_max;
            
            try
            {
                x_min = ds.XMin();
                x_max = ds.XMax();
                y_min = ds.YMin();
                y_max = ds.YMax();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Cannot set global Min-Max: " + ex.Message);
                Console.WriteLine("Setting default values...");
                x_min = 1000.0;
                x_max = 1900.0;
                y_min = 0.0;
                y_max = 1.0;
            }
            
            this.SetGlobalXMinMax(x_min, x_max);
            this.SetGlobalYMinMax(y_min, y_max);
        }
        
        public void GetXMinMax(double pan, double zoom, out double min, out double max)
        {
            double global_range = this.ExtendedGlobalXMax - this.ExtendedGlobalXMin;
            double zoomed_range = global_range / zoom;
            double panned_range = global_range - zoomed_range;
                
            if (this.reverse_x_axes)
            {
                max = this.GlobalXMax - pan * panned_range;
                min = this.GlobalXMax - pan * panned_range - zoomed_range;
            }
            else
            {
                min = this.GlobalXMin + pan * panned_range;
                max = this.GlobalXMin + pan * panned_range + zoomed_range;
            }
        }
        
        public void GetYMinMax(double pan, double zoom, out double min, out double max)
        {
            double y_min = this.ExtendedGlobalYMin;
            double y_max = this.ExtendedGlobalYMax;
            double global_range = y_max - y_min;
            double zoomed_range = global_range / zoom;
            double panned_range = global_range - zoomed_range;
            
            min = y_min + (1.0 - pan) * panned_range;
            max = y_min + (1.0 - pan) * panned_range + zoomed_range;
        }
        
        public void SetXAxis(double pan, double zoom)
        {
            this.GetXMinMax(pan, zoom, out double new_world_min, out double new_world_max);

            if (Math.Abs(this.XMin - new_world_min) > EPSILON)
                this.XMin = new_world_min;

            if (Math.Abs(this.XMax - new_world_max) > EPSILON)
                this.XMax = new_world_max;
        }

        public void SetYAxis(double pan, double zoom)
        {
            this.GetYMinMax(pan, zoom, out double new_world_min, out double new_world_max);

            if (Math.Abs(this.YMin - new_world_min) > EPSILON)
                this.YMin = new_world_min;

            if (Math.Abs(this.YMax - new_world_max) > EPSILON)
                this.YMax = new_world_max;
        }
        
        public double GetWorldXCoord(int x)
        {
            return this.GetWorldXCoord(x, true);
        }
        
        public double GetWorldYCoord(int y)
        {
            return this.GetWorldYCoord(y, true);
        }

        public double GetWorldXCoord(int x, bool clip)
        {
            PointF p = new PointF((float)x, 0f);
            PointF p_min = this.plotsurface.PhysicalXAxis1Cache.PhysicalMin;
            PointF p_max = this.plotsurface.PhysicalXAxis1Cache.PhysicalMax;
            return this.plotsurface.XAxis1.PhysicalToWorld(p, p_min, p_max, clip);
        }
        
        public double GetWorldYCoord(int y, bool clip)
        {
            PointF p = new PointF(0f, (float)y);
            PointF p_min = this.plotsurface.PhysicalYAxis1Cache.PhysicalMin;
            PointF p_max = this.plotsurface.PhysicalYAxis1Cache.PhysicalMax;
            return this.plotsurface.YAxis1.PhysicalToWorld(p, p_min, p_max, clip);
        }
        
        public int GetPhysicalXCoord(double x)
        {
            return this.GetPhysicalXCoord(x, true);
        }
        
        public int GetPhysicalYCoord(double y)
        {
            return this.GetPhysicalYCoord(y, true);
        }
        
        public int GetPhysicalXCoord(double x, bool clip)
        {
            PointF p_min = this.plotsurface.PhysicalXAxis1Cache.PhysicalMin;
            PointF p_max = this.plotsurface.PhysicalXAxis1Cache.PhysicalMax;
            return (int)this.plotsurface.XAxis1.WorldToPhysical(x, p_min, p_max, clip).X;
        }

        public int GetPhysicalYCoord(double y, bool clip)
        {
            PointF p_min = this.plotsurface.PhysicalYAxis1Cache.PhysicalMin;
            PointF p_max = this.plotsurface.PhysicalYAxis1Cache.PhysicalMax;
            return (int)this.plotsurface.YAxis1.WorldToPhysical(y, p_min, p_max, clip).Y;
        }

        public Gdk.Pixbuf GetPixbuf()
        {
            return this.GetPixbuf(this.plotsurface.Width, this.plotsurface.Height);
        }
        
        public Gdk.Pixbuf GetPixbuf(int width, int height)
        {
            Bitmap bmp = this.GetBitmap(width, height);
            MemoryStream stream = new MemoryStream();
            bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            
            return new Gdk.Pixbuf(stream.ToArray());
        }
        
        public Gdk.Pixbuf GetPixbuf(float xdpi, float ydpi)
        {
            return this.GetPixbuf(this.plotsurface.Width, this.plotsurface.Height, xdpi, ydpi);
        }
        
        public Gdk.Pixbuf GetPixbuf(int width, int height, float xdpi, float ydpi)
        {
            Bitmap bmp = this.GetBitmap(width, height, xdpi, ydpi);
            MemoryStream stream = new MemoryStream();
            bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            
            return new Gdk.Pixbuf(stream.ToArray());
        }
        
        public Gdk.Pixbuf GetLineStylePixbuf(Spectrum spectrum)
        {
            Bitmap bmp = new Bitmap(50, 11);
            Graphics g = Graphics.FromImage(bmp);
            Pen p = new Pen(spectrum.Color, spectrum.LineWidth)
            {
                DashStyle = (DashStyle)Enum.Parse(typeof(DashStyle), spectrum.LineStyle.ToString())
            };
            g.DrawLine(p, 0f, 5f, 50f, 5f);
            MemoryStream stream = new MemoryStream();
            bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            
            return new Gdk.Pixbuf(stream.ToArray());
        }
        
        public Gdk.Pixbuf GetLineStylePixbuf(LineStyle style)
        {
            Bitmap bmp = new Bitmap(50, 11);
            Graphics g = Graphics.FromImage(bmp);
            Pen p = new Pen(Color.Black, this.DefaultLineWidth)
            {
                DashStyle = (DashStyle)Enum.Parse(typeof(DashStyle), style.ToString())
            };
            g.DrawLine(p, 5f, 5f, 45f, 5f);
            MemoryStream stream = new MemoryStream();
            bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            
            return new Gdk.Pixbuf(stream.ToArray());
        }

        public Gdk.Pixbuf GetLineStylePixbuf(CapStyle style)
        {
            Bitmap bmp = new Bitmap(50, 11);
            Graphics g = Graphics.FromImage(bmp);
            Pen p = new Pen(Color.Black, this.DefaultLineWidth);
            g.DrawLine(p, 5f, 5f, 45f, 5f);
            p = new Pen(Color.Black, this.DefaultLineWidth)
            {
                CustomStartCap = LineCaps.GetLineCap(style, this.DefaultLineWidth)
            };
            g.DrawLine(p, 5f, 5f, 6f, 5f);
            MemoryStream stream = new MemoryStream();
            bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            
            return new Gdk.Pixbuf(stream.ToArray());
        }
        
        public Bitmap GetBitmap()
        {
            return this.GetBitmap(this.plotsurface.Width, this.plotsurface.Height);
        }
        
        public Bitmap GetBitmap(int width, int height, Color color)
        {
            Bitmap bmp = new Bitmap(width, height);
            Graphics g = Graphics.FromImage(bmp);
            g.Clear(color);
            this.Draw(g, new System.Drawing.Rectangle(0, 0, bmp.Width-1, bmp.Height-1));
            this.DrawOverlays(g, true);
            this.DrawLegend(g);
            return bmp;
        }
        
        public Bitmap GetBitmap(int width, int height)
        {
            return this.GetBitmap(width, height, Color.White);
        }
        
        public Bitmap GetBitmap(float xdpi, float ydpi)
        {
            return this.GetBitmap(this.plotsurface.Width, this.plotsurface.Height, xdpi, ydpi);
        }
        
        public Bitmap GetBitmap(int width, int height, float xdpi, float ydpi)
        {
            PlatformID osversion = Environment.OSVersion.Platform;
            if (osversion != PlatformID.Win32NT      &&
                osversion != PlatformID.Win32S       &&
                osversion != PlatformID.Win32Windows &&
                osversion != PlatformID.WinCE)
                return this.GetBitmap(width, height);
            
            Bitmap bmp = new Bitmap((int)Math.Round(width *xdpi/72f), 
                                    (int)Math.Round(height*ydpi/72f));
            bmp.SetResolution(xdpi, ydpi);
            Graphics g = Graphics.FromImage(bmp);
            g.PageUnit  = GraphicsUnit.Point;
            g.Clear(Color.White);
            this.Draw(g, new System.Drawing.Rectangle(0, 0,
                                       (int)Math.Round((bmp.Width -1)*72f/ xdpi),
                                       (int)Math.Round((bmp.Height-1)*72f/ ydpi)));
            this.DrawOverlays(g, true);
            this.DrawLegend(g);
            return bmp;
        }

        public Cairo.Surface GetCairoSurface()
        {
            Cairo.Context context = Gdk.CairoHelper.Create(this.GtkWidget.GdkWindow);
            Cairo.Surface surface = context.GetTarget();

            ((IDisposable)context).Dispose();

            return surface;
        }
        
        public Cairo.Context GetCairoContext()
        {
            Cairo.Context context = Gdk.CairoHelper.Create(this.GtkWidget.GdkWindow);
            Cairo.Surface surface = context.GetTarget();

            Cairo.Context cr = new Cairo.Context(surface);
            
            ((IDisposable)context).Dispose();

            return cr;
        }

        public void Draw(Graphics g, System.Drawing.Rectangle bounds)
        {
            if (this.EstimatedLegendHeight > bounds.Height || this.LegendVisible == false)
                this.plotsurface.Legend = null;
            else
                this.plotsurface.Legend = this.legend;
            
            this.plotsurface.Draw(g, bounds);
        }
        
        public void DrawLegend(Graphics g)
        {
            this.plotsurface.DrawLegend(g);
        }

        public void DrawOverlays(Graphics g, bool only_static_overlays)
        {
            if (this.selection != null)
            {
                if ((only_static_overlays && this.selection.IsStatic) ||
                    only_static_overlays == false)
                    this.selection.Draw(g);
                else
                    this.AsyncRefresh(true);
            }
            
            if (this.horizontal_selection != null)
            {
                if ((only_static_overlays && this.horizontal_selection.IsStatic) ||
                    only_static_overlays == false)
                    this.horizontal_selection.Draw(g);
                else
                    this.AsyncRefresh(true);
            }

            if (this.vertical_selection != null)
            {
                if ((only_static_overlays && this.vertical_selection.IsStatic) ||
                    only_static_overlays == false)
                    this.vertical_selection.Draw(g);
                else
                    this.AsyncRefresh(true);
            }
            
            lock (this.data_set_selections)
            {
                foreach (DataSetSelection dss in this.data_set_selections)
                {
                    dss.Draw(g);
                }
            }

            lock (this.markers)
            {
                if (this.markers.Count > 0)
                {
                    int x_min = this.PhysicalXMin;
                    int x_max = this.PhysicalXMax;
                    int y_min = this.PhysicalYMax;
                    int y_max = this.PhysicalYMin;
                    
                    Pen p = new Pen(Color.Black);
                    SolidBrush b = new SolidBrush(Color.Black);
                    
                    foreach (CrossHair marker in this.markers)
                    {
                        PointF pt1 = new PointF(x_min, marker.Y);
                        PointF pt2 = new PointF(x_max, marker.Y);
                        g.DrawLine(p, pt1, pt2);
                        
                        pt1 = new PointF(marker.X, y_min);
                        pt2 = new PointF(marker.X, y_max);
                        g.DrawLine(p, pt1, pt2);

                        System.Drawing.Rectangle r = new System.Drawing.Rectangle(marker.X+5, marker.Y-35, 200, 35);
                        string s = string.Format("x={0}\ny={1}", 
                                                 this.GetWorldXCoord(marker.X).ToString("f2"),
                                                 this.GetWorldYCoord(marker.Y).ToString("f2"));
                        g.DrawString(s, new Font("Arial", 10f), b, r);
                    }
                }
            }

            lock (this.annotations)
            {
                if (this.annotations.Count > 0)
                {
                    SmoothingMode sav_smoothing_mode = g.SmoothingMode;
                    g.SmoothingMode = SmoothingMode.AntiAlias;
                    
                    foreach (Annotation a in this.annotations)
                    {
                        a.Draw(g);
                    }
                    
                    g.SmoothingMode = sav_smoothing_mode;
                }
            }
        }

        public void SetSelection(int x, int y, int width, int height)
        {
            if (this.disable_selections)
                return;

            try
            {
                this.selection = new RubberBandSelection(this, x, y, width, height);
                this.selection.Changed += (o, e) => this.AsyncRefresh(true);
                this.Refresh(true);
            }
            catch (NullReferenceException)
            {
                // axes may not be initialized
                this.selection = null;
            }
        }
        
        public void RequestUpdate()
        {
            this.data_sets_changed = true;
        }
        
        public void AsyncRefresh()
        {
            this.refresh_is_pending = true;
        }
        
        public void AsyncRefresh(bool only_overlays)
        {
            this.refresh_overlay_is_pending = true;
        }
        
        public void StartAutoUpdate()
        {
            if (this.__thread_running)
                return;

            this.do_process_changes = new Thread(new ThreadStart(this.__DoProcessChanges))
            {
                IsBackground = true
            };
            this.__thread_running = true;
            this.do_process_changes.Start();
        }
        
        public void StopAutoUpdate()
        {
            this.__thread_running = false;
        }
        
        public void SetBackgroundPlot(IPlotEngine iplot, BackgroundType bg_type)
        {
            NPlotEngine plot = iplot as NPlotEngine;
            
            lock (this)
            {
                bool sav_limits = false;
                double xmin = 0.0, xmax = 0.0, ymin = 0.0, ymax = 0.0;
                if (this.plotsurface.XAxis2 != null && 
                    this.plotsurface.YAxis2 != null &&
                    double.IsInfinity(this.plotsurface.XAxis2.WorldMin) == false &&
                    double.IsNaN(this.plotsurface.XAxis2.WorldMin) == false)
                {
                    xmin = this.plotsurface.XAxis2.WorldMin;
                    xmax = this.plotsurface.XAxis2.WorldMax;
                    ymin = this.plotsurface.YAxis2.WorldMin;
                    ymax = this.plotsurface.YAxis2.WorldMax;
                    sav_limits = true;
                }
                
                this.__background_data_sets.Clear();
                
                foreach (string id in plot.DataSets.Keys)
                {
                    if (plot.DataSets[id].IsVisible == false || plot.DataSets[id].IsPreview)
                        continue;

                    DataSet ds = new DataSet(plot.DataSets[id])
                    {
                        XLabel = plot.DataSets[id].XLabel,
                        YLabel = plot.DataSets[id].YLabel,
                        Label = plot.DataSets[id].Label + "*",
                        Id = plot.DataSets[id].Id + "%%%background%%%",
                        Color = plot.DataSets[id].Color,
                        LineStyle = plot.DataSets[id].LineStyle,
                        LineWidth = plot.DataSets[id].LineWidth,
                        IsVisible = plot.DataSets[id].IsVisible,
                        ZOrder = plot.DataSets[id].ZOrder
                    };

                    switch (bg_type)
                    {
                        
                    case BackgroundType.Unchanged:
                        break;
                        
                    case BackgroundType.Grayscaled:
                        Color c = ds.Color;
                        int luma = (int)(c.R*0.3f + c.G*0.59f + c.B*0.11f);
                        ds.Color = Color.FromArgb(luma, luma, luma);
                        break;
                        
                    case BackgroundType.Dashed:
                        ds.LineStyle = LineStyle.Dash;
                        break;
                        
                    }
                    
                    ds.IsBackground = true;
                    
                    this.__background_data_sets.Add(id, ds);
                }
                
                this.xaxis2.Label = plot.XLabel + "*";
                this.yaxis2.Label = plot.YLabel + "*";

                if (sav_limits)
                {
                    this.xaxis2.WorldMin = xmin;
                    this.xaxis2.WorldMax = xmax;
                    this.yaxis2.WorldMin = ymin;
                    this.yaxis2.WorldMax = ymax;
                }
                else
                {
                    this.xaxis2.WorldMin = plot.xaxis1.WorldMin;
                    this.xaxis2.WorldMax = plot.xaxis1.WorldMax;
                    this.yaxis2.WorldMin = plot.yaxis1.WorldMin;
                    this.yaxis2.WorldMax = plot.yaxis1.WorldMax;
                }
                
                this.plotsurface.XAxis2 = new LinearAxis(this.xaxis2);
                this.plotsurface.YAxis2 = new LinearAxis(this.yaxis2);

                this.xaxis_changed = true;
                this.yaxis_changed = true;
            }
        }
        
        public void ClearBackgroundPlot()
        {
            lock (this)
                this.__background_data_sets.Clear();
        }
        
        public void SyncAxes()
        {
            lock (this)
            {
                this.xaxis2.WorldMin = this.xaxis1.WorldMin;
                this.xaxis2.WorldMax = this.xaxis1.WorldMax;
                this.yaxis2.WorldMin = this.yaxis1.WorldMin;
                this.yaxis2.WorldMax = this.yaxis1.WorldMax;
                this.xaxis_changed = true;
                this.yaxis_changed = true;
            }
        }
    }
}