// 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.Spectroscopy;

using NPlot;


namespace Hiperscan.NPlotEngine
{

    public partial class NPlotEngine
    {
        private readonly PlotSurface2D __thumbnail_plotsurface;
        private readonly Bitmap __thumbnail_bitmap;
        private Gdk.Pixbuf __thumbnail_pixbuf;
        private const int thumbnail_width  = 60;
        private const int thumbnail_height = 40;
        private readonly DrawableInfoList __drawables;
        private readonly Queue __removed_drawables;
        private readonly Queue __added_drawables;
        private readonly Queue __added_bg_drawables;
        private Dictionary<string,DataSet> __data_sets;
        private readonly Dictionary<string,DataSet> __background_data_sets;
        private volatile bool __thread_running = false;

        private Thread do_process_changes;

        
        private void __ProcessChanges()
        {
            this.__ProcessAxesChanges();
            this.__ProcessDataSets();
            this.__ProcessDrawables();
            this.__ProcessRefreshs();
            this.__ProcessDelegates();
        }
        
        private void __DoProcessChanges()
        {
            while (true)
            {
                this.__ProcessAxesChanges();
                this.__ProcessDataSets();
                this.__ProcessDrawables();
                this.__ProcessRefreshs();
                this.__ProcessDelegates();
                
                if (this.__thread_running == false)
                    break;

                Thread.Sleep(20);
            }
        }
        
        private void __ProcessAxesChanges()
        {
            if (this.xaxis_changed)
            {
                double new_min1, new_max1, new_min2, new_max2;
                string new_label1, new_label2;
                lock (this)
                {
                    new_min1   = this.xaxis1.WorldMin;
                    new_max1   = this.xaxis1.WorldMax;
                    new_label1 = this.xaxis1.Label;
                    new_min2   = this.xaxis2.WorldMin;
                    new_max2   = this.xaxis2.WorldMax;
                    new_label2 = this.xaxis2.Label;
                    this.xaxis_changed = false;
                }

                lock (this.plotsurface)
                {
                    if (this.plotsurface.XAxis1 != null)
                    {
                        if (Math.Abs(plotsurface.XAxis1.WorldMin - new_min1) > EPSILON)
                        {
                            plotsurface.XAxis1.WorldMin = new_min1;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (Math.Abs(plotsurface.XAxis1.WorldMax - new_max1) > EPSILON)
                        {
                            plotsurface.XAxis1.WorldMax = new_max1;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (plotsurface.XAxis1.Label != new_label1)
                        {
                            plotsurface.XAxis1.Label = new_label1;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (plotsurface.XAxis1.Reversed != this.reverse_x_axes)
                        {
                            plotsurface.XAxis1.Reversed = this.reverse_x_axes;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }
                    }
                    
                    if (this.plotsurface.XAxis2 != null)
                    {
                        if (Math.Abs(plotsurface.XAxis2.WorldMin - new_min2) > EPSILON)
                        {
                            plotsurface.XAxis2.WorldMin = new_min2;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (Math.Abs(plotsurface.XAxis2.WorldMax - new_max2) > EPSILON)
                        {
                            plotsurface.XAxis2.WorldMax = new_max2;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (plotsurface.XAxis2.Label != new_label2)
                        {
                            plotsurface.XAxis2.Label = new_label2;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (plotsurface.XAxis2.Reversed != this.reverse_x_axes)
                        {
                            plotsurface.XAxis2.Reversed = this.reverse_x_axes;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }
                    }
                }
            }
            
            if (this.yaxis_changed)
            {
                double new_min1, new_max1, new_min2, new_max2;
                string new_label1, new_label2;
                lock (this)
                {
                    new_min1   = this.yaxis1.WorldMin;
                    new_max1   = this.yaxis1.WorldMax;
                    new_label1 = this.yaxis1.Label;
                    new_min2   = this.yaxis2.WorldMin;
                    new_max2   = this.yaxis2.WorldMax;
                    new_label2 = this.yaxis2.Label;
                    this.yaxis_changed = false;
                }

                lock (this.plotsurface)
                {
                    if (this.plotsurface.YAxis1 != null)
                    {
                        if (Math.Abs(plotsurface.YAxis1.WorldMin - new_min1) > EPSILON)
                        {
                            plotsurface.YAxis1.WorldMin = new_min1;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (Math.Abs(plotsurface.YAxis1.WorldMax - new_max1) > EPSILON)
                        {
                            plotsurface.YAxis1.WorldMax = new_max1;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (plotsurface.YAxis1.Label != new_label1)
                        {
                            plotsurface.YAxis1.Label = new_label1;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }
                    }
                    
                    if (this.plotsurface.YAxis2 != null)
                    {
                        if (Math.Abs(plotsurface.YAxis2.WorldMin - new_min2) > EPSILON)
                        {
                            plotsurface.YAxis2.WorldMin = new_min2;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (Math.Abs(plotsurface.YAxis2.WorldMax - new_max2) > EPSILON)
                        {
                            plotsurface.YAxis2.WorldMax = new_max2;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }

                        if (plotsurface.YAxis2.Label != new_label2)
                        {
                            plotsurface.YAxis2.Label = new_label2;
                            this.refresh_is_pending = true;
                            this.plot_changed = true;
                        }
                    }
                }
            }
        }
        
        private void __ProcessDataSets()
        {
            lock (this)
            {
                if (this.data_sets_changed == false)
                    return;

                this.data_sets_changed = false;
                
                var current_drawables = new Dictionary<Guid,DrawableInfo>(this.__drawables);

                if (this.__data_sets.Count == 0)
                {
                    Application.Invoke((sender, e) =>
                    {
                        this.DataSetSelections.Clear();
                        this.Clear();
                    });
                    return;
                }

                var data_sets = new List<DataSet>(this.__background_data_sets.Values);
                data_sets.AddRange(this.__data_sets.Values);
                    
                foreach (DataSet ds in data_sets)
                {
                    if (ds.IsVisible == false)
                        continue;
                    
                    if (this.__drawables.ContainsKey(ds.Guid))
                    {
                        // data set is allready up-to-date
                        current_drawables.Remove(ds.Guid);
                        this.__drawables[ds.Guid].ZOrder = ds.ZOrder;
                        continue;
                    }
                    
                    // its a new or updated data set
                    Guid found_guid = Guid.Empty;
                    foreach (Guid guid in this.__drawables.Keys)
                    {
                        if (this.__drawables[guid].Id == ds.Id)
                        {
                            found_guid = guid;
                            break;
                        }
                    }
                        
                    if (found_guid != Guid.Empty)
                    {
                        this.__removed_drawables.Enqueue(this.__drawables[found_guid].Drawable);
                        this.__drawables.Remove(found_guid);
                    }
                        
                    IDrawable drawable = this.__CreateDrawable(ds);
                    
                    if (ds.IsBackground)
                        this.__added_bg_drawables.Enqueue(drawable);
                    else
                        this.__added_drawables.Enqueue(drawable);
                    
                    this.__drawables.Add(ds.Guid, new DrawableInfo(drawable, ds));
                    
                    this.SetGlobalMinMax(ds);
                    
                    this.drawable_changed = true;
                }
                
                foreach (DrawableInfo drawable in current_drawables.Values)
                {
                    this.__removed_drawables.Enqueue(drawable.Drawable);
                    this.__drawables.Remove(drawable.Guid);
                    this.drawable_changed = true;
                }
                
                this.__CreateThumbnail();
            }
        }
        
        private void __ProcessDrawables()
        {
            lock (this)
            {
                if (!this.drawable_changed)
                    return;
                
                this.drawable_changed = false;
                bool added   = false;
                bool changed = false;

                if (this.is_empty)
                {
                    this.is_empty = false;
                    
                    this.plotsurface.SmoothingMode = SmoothingMode.AntiAlias;

                    this.plotsurface.XAxis1 = new LinearAxis(this.xaxis1);
                    this.plotsurface.YAxis1 = new LinearAxis(this.yaxis1);
                    this.plotsurface.XAxis2 = new LinearAxis(this.xaxis2);
                    this.plotsurface.YAxis2 = new LinearAxis(this.yaxis2);
                    
                    if (this.reverse_x_axes)
                    {
                        this.plotsurface.XAxis1.Reversed = true;
                        this.plotsurface.XAxis2.Reversed = true;
                    }
                    
                    switch (this.type)
                    {
                        
                    case PlotEngineType.Normal:
                    case PlotEngineType.Synchronous:
                        this.plotsurface.Add(this.grid);
                        Application.Invoke((sender, e) =>
                        {
                            if (this.GtkWidget.GdkWindow != null)
                                this.GtkWidget.GdkWindow.Cursor = new Gdk.Cursor(Gdk.CursorType.Tcross);
                        });
                        break;
                        
                    case PlotEngineType.Preview:
                        this.plotsurface.XAxis1.Hidden = true;
                        this.plotsurface.YAxis1.Hidden = true;
                        this.plotsurface.XAxis2.Hidden = true;
                        this.plotsurface.YAxis2.Hidden = true;
                        this.plotsurface.Legend = null;
                        break;
                        
                    }
                }
                
                if (this.__background_data_sets.Count == 0)
                {
                    this.plotsurface.XAxis2 = null;
                    this.plotsurface.YAxis2 = null;
                }
                
                if (this.__removed_drawables.Count > 0)
                {
                    while (this.__removed_drawables.Count > 0)
                    {
                        IDrawable drawable = (IDrawable)this.__removed_drawables.Dequeue();
                        if (drawable == null)
                            continue;
                        this.plotsurface.Remove(drawable, false);
                        drawable = null;
                        this.refresh_is_pending = true;
                    }

                    if (this.DataSetRemoved != null)
                        Application.Invoke((sender, e) => this.DataSetRemoved(this, EventArgs.Empty));

                    changed = true;
                }
                
                if (this.__added_bg_drawables.Count > 0)
                {
                    while (this.__added_bg_drawables.Count > 0)
                    {
                        IDrawable drawable = (IDrawable)this.__added_bg_drawables.Dequeue();
                        if (drawable == null)
                            continue;

                        this.plotsurface.Add(drawable, 
                                             NPlot.PlotSurface2D.XAxisPosition.Top,
                                             NPlot.PlotSurface2D.YAxisPosition.Right);
                        this.plotsurface.XAxis2.WorldMin = this.xaxis2.WorldMin;
                        this.plotsurface.XAxis2.WorldMax = this.xaxis2.WorldMax;
                        this.plotsurface.YAxis2.WorldMin = this.yaxis2.WorldMin;
                        this.plotsurface.YAxis2.WorldMax = this.yaxis2.WorldMax;
                        this.refresh_is_pending = true;
                    }

                    added   = true;
                    changed = true;
                }

                if (this.__added_drawables.Count > 0)
                {
                    while (this.__added_drawables.Count > 0)
                    {
                        IDrawable drawable = (IDrawable)this.__added_drawables.Dequeue();
                        if (drawable == null)
                            continue;

                        if (this.use_second_axes)
                        {
                            this.plotsurface.Add(drawable, 
                                                 NPlot.PlotSurface2D.XAxisPosition.Bottom,
                                                 NPlot.PlotSurface2D.YAxisPosition.Right);
                            this.plotsurface.YAxis2.WorldMin = this.yaxis2.WorldMin;
                            this.plotsurface.YAxis2.WorldMax = this.yaxis2.WorldMax;
                        }
                        else
                        {
                            this.plotsurface.Add(drawable,
                                                 NPlot.PlotSurface2D.XAxisPosition.Bottom,
                                                 NPlot.PlotSurface2D.YAxisPosition.Left);
                            this.plotsurface.YAxis1.WorldMin = this.yaxis1.WorldMin;
                            this.plotsurface.YAxis1.WorldMax = this.yaxis1.WorldMax;
                        }
                        this.plotsurface.XAxis1.WorldMin = this.xaxis1.WorldMin;
                        this.plotsurface.XAxis1.WorldMax = this.xaxis1.WorldMax;
                        this.refresh_is_pending = true;
                    }
                    
                    added   = true;
                    changed = true;
                }
                
                if (added && this.DataSetAdded != null)
                    Application.Invoke((sender, e) => this.DataSetAdded(this, EventArgs.Empty));
                
                if (changed && this.DataSetsChanged != null)
                    Application.Invoke((sender, e) => this.DataSetsChanged(this, EventArgs.Empty));
                
                this.plot_changed = changed;
                
                // assemble IDrawables in right order
                if (changed)
                {
                    lock (this.plotsurface)
                    {
                        List<Guid> guids = this.__drawables.GetSortedGuids();
                        
                        List<object> xaxis_pos = new List<object>(this.plotsurface.XAxisPositions.ToArray());
                        List<object> yaxis_pos = new List<object>(this.plotsurface.YAxisPositions.ToArray());
                        List<object> drawables = new List<object>(this.plotsurface.Drawables.ToArray());
                        
                        int ix = 0;
                        for (int jx=0; jx < drawables.Count; ++jx)
                        {
                            if (drawables[jx] is Grid)
                            {
                                this.plotsurface.XAxisPositions[ix] = xaxis_pos[jx];
                                this.plotsurface.YAxisPositions[ix] = yaxis_pos[jx];
                                this.plotsurface.Drawables[ix++]    = drawables[jx];
                                xaxis_pos.RemoveAt(jx);
                                yaxis_pos.RemoveAt(jx);
                                drawables.RemoveAt(jx);
                                break;
                            }
                        }
                        
                        foreach (Guid guid in guids)
                        {
                            DrawableInfo info = this.__drawables[guid];
                            
                            if (drawables.Contains(info.Drawable))
                            {
                                int jx = drawables.IndexOf(info.Drawable);
                                this.plotsurface.XAxisPositions[ix] = xaxis_pos[jx];
                                this.plotsurface.YAxisPositions[ix] = yaxis_pos[jx];
                                this.plotsurface.Drawables[ix++]    = drawables[jx];
                                xaxis_pos.RemoveAt(jx);
                                yaxis_pos.RemoveAt(jx);
                                drawables.RemoveAt(jx);
                            }
                        }
                        
                        for (int jx=0; jx < drawables.Count; ++jx)
                        {
                            this.plotsurface.XAxisPositions[ix] = xaxis_pos[jx];
                            this.plotsurface.YAxisPositions[ix] = yaxis_pos[jx];
                            this.plotsurface.Drawables[ix++]    = drawables[jx];
                        }
                    }
                    
                    this.plotsurface.CheckLegendSize();
                }
            }
        }
        
        private void __ProcessRefreshs()
        {
            while (this.refresh_is_pending || this.refresh_overlay_is_pending)
            {
                lock (this)
                {
                    if (this.refresh_is_pending)
                    {
                        this.refresh_is_pending = false;
                        this.refresh_overlay_is_pending = false;
                        this.plotsurface.UpdateCache();
                    }
                    else
                    {
                        this.refresh_overlay_is_pending = false;
                        this.plotsurface.UpdateCache(true);
                    }
                }
                
                if (this.refresh_is_pending || this.refresh_overlay_is_pending)
                    continue;
                
                Application.Invoke((sender, e) =>
                {
                    this.plotsurface.QueueDrawArea();
                    this.Refreshed?.Invoke(this, EventArgs.Empty);
                });
            }
        }
        
        private void __ProcessDelegates()
        {
            if (this.plot_changed == false)
                return;

            this.plot_changed = false;
            
            if (this.PlotChanged != null)
                Application.Invoke((sender, e) => this.PlotChanged(this, EventArgs.Empty));
            
            if (this.created == false && this.PlotCreated != null)
            {
                this.created = true;
                Application.Invoke((sender, e) => this.PlotCreated(this, EventArgs.Empty));
            }
        }
        
        private IDrawable __CreateDrawable(DataSet ds)
        {
            if (ds.Color == Color.Empty)
                ds.Color = Color.Blue;
            
            if (ds.LineWidth < 1)
                ds.LineWidth = this.DefaultLineWidth;
            
            if (ds.IsContinuous)
            {
                LinePlot lp = new LinePlot
                {
                    AbscissaData = ds.X,
                    OrdinateData = ds.Y,
                    Gaps = ds.Gaps,
                    Pen  = new Pen(ds.Color, ds.LineWidth)
                };
                lp.Pen.DashStyle = (DashStyle)Enum.Parse(typeof(DashStyle), ds.LineStyle.ToString());

                if (string.IsNullOrEmpty(ds.Label) == false)
                    lp.Label = ds.Label;

                if (ds.IsRegion == false)
                    return lp;

                DataSet limit_ds = ds * 0.0 + ds.RegionFillLimit;

                LinePlot limit_lp = new LinePlot
                {
                    AbscissaData = limit_ds.X,
                    OrdinateData = limit_ds.Y,
                    Pen = lp.Pen
                };
                FilledRegion fr = new FilledRegion(limit_lp, lp)
                {
                    Brush = new HatchBrush(HatchStyle.BackwardDiagonal, ds.Color, Color.Transparent)
                };
                return fr;
            }
            else
            {
                PointPlot pp;
                
                if (ds.IsDropPoint)
                {
                    pp = new PointPlot();
                    pp.Marker.DropLine = true;
                    pp.Marker.Pen = Pens.Red;
                    pp.Marker.Filled = false;
                    pp.Marker.Type = (Marker.MarkerType)Enum.Parse(typeof(Marker.MarkerType), ds.MarkerStyle.ToString());
                    pp.AbscissaData = ds.X;
                    pp.OrdinateData = ds.Y;
                    
                    if (string.IsNullOrEmpty(ds.Label) == false)
                        pp.Label = ds.Label;
                }
                else
                {
                    Marker.MarkerType mt = (Marker.MarkerType)Enum.Parse(typeof(Marker.MarkerType), ds.MarkerStyle.ToString());
                    Marker m = new Marker(mt, 6, new Pen(ds.Color, this.DefaultLineWidth));
                    pp = new PointPlot(m)
                    {
                        AbscissaData = ds.X,
                        OrdinateData = ds.Y
                    };
                    if (string.IsNullOrEmpty(ds.Label) == false)
                        pp.Label = ds.Label;
                }
                
                return pp;
            }
        }
        
        private void __ResetThumbnail()
        {
            using (Graphics g = Graphics.FromImage(this.__thumbnail_bitmap))
            {
                g.Clear(Color.Transparent);
            }
            
            MemoryStream stream = new MemoryStream();
            this.__thumbnail_bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            this.__thumbnail_pixbuf = new Gdk.Pixbuf(stream.ToArray());
        }
        
        private void __ErrorThumbnail()
        {
            using (Graphics g = Graphics.FromImage(this.__thumbnail_bitmap))
            {
                g.Clear(Color.LightGray);
            }
            
            MemoryStream stream = new MemoryStream();
            this.__thumbnail_bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            this.__thumbnail_pixbuf = new Gdk.Pixbuf(stream.ToArray());
        }
        
        private void __CreateThumbnail()
        {
            try
            {
                if (this.__data_sets.Count == 0)
                    return;
            
                this.__thumbnail_plotsurface.Clear();
                this.__thumbnail_plotsurface.PlotBackColor = Color.Transparent;
                
                double gxmin = double.MaxValue;
                double gxmax = double.MinValue;
                double gymin = double.MaxValue;
                double gymax = double.MinValue;
                
                bool empty = true;
                
                foreach (DataSet ds in this.DataSets.Values)
                {
                    if (ds.IsVisible == false || ds.IsPreview)
                        continue;
                    
                    LinePlot lp = new LinePlot();
                    
                    double xmin = ds.XMin();
                    double xmax = ds.XMax();
                    
                    double ymin, ymax;
                    try
                    {
                        ymin = ds.YMin();
                        ymax = ds.YMax();
                    }
                    catch (NotFiniteNumberException)
                    {
                        ymin = gymin;
                        ymax = gymax;
                    }
                    
                    if (xmin < gxmin) gxmin = xmin;
                    if (xmax > gxmax) gxmax = xmax;
                    if (ymin < gymin) gymin = ymin;
                    if (ymax > gymax) gymax = ymax;
                    
                    lp.AbscissaData = ds.X;
                    lp.OrdinateData = ds.Y;
                    lp.Pen = new Pen(ds.Color, 1f);
                    
                    this.__thumbnail_plotsurface.Add(lp);
                    empty = false;
                }
                
                if (empty)
                {
                    this.__ResetThumbnail();
                    return;
                }
                
                if (double.IsInfinity(gymin) || double.IsInfinity(gymax))
                {
                    gymin = 0.0;
                    gymax = 1.0;
                }
                
                this.__thumbnail_plotsurface.XAxis1.WorldMin = gxmin;
                this.__thumbnail_plotsurface.XAxis1.WorldMax = gxmax;
                this.__thumbnail_plotsurface.YAxis1.WorldMin = gymin;
                this.__thumbnail_plotsurface.YAxis1.WorldMax = gymax;
                this.__thumbnail_plotsurface.XAxis1.Hidden = true;
                this.__thumbnail_plotsurface.YAxis1.Hidden = true;
                this.__thumbnail_plotsurface.XAxis1.Reversed = this.reverse_x_axes;
                
                using (Graphics g = Graphics.FromImage(this.__thumbnail_bitmap))
                {
                    g.Clear(Color.Transparent);
                    this.__thumbnail_plotsurface.Draw(g, new Rectangle(-8, 0, thumbnail_width+16, thumbnail_height));
                    g.DrawRectangle(new Pen(Brushes.LightGray), 0, 8, thumbnail_width-1, thumbnail_height-15);
                }
                
                MemoryStream stream = new MemoryStream();
                this.__thumbnail_bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
                
                this.__thumbnail_pixbuf = new Gdk.Pixbuf(stream.ToArray());
            }
            catch (Exception ex)
            {
                // if something goes wrong, show error thumbnail
                Console.WriteLine("Exception in NPlotEngine.__CreateThumbnail: " + ex.ToString());
                this.__ErrorThumbnail();
            }
        }

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