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

using Gtk;

using NPlot;


namespace Hiperscan.NPlotEngine
{

    public class PlotSurface2D : Gtk.DrawingArea, NPlot.IPlotSurface2D
    {
        private readonly NPlot.PlotSurface2D __ps;

        private Bitmap bitmap_cache;
        private Bitmap bitmap_without_overlays_cache;
        private Gdk.Rectangle current_allocation;
        private readonly Graphics graphics_helper = Graphics.FromImage(new Bitmap(100, 100));

        private volatile bool is_allocated = false;
        private volatile int update_counter = 0;
        private readonly bool no_gtk_dotnet = false;
        private readonly bool is_unix = Environment.OSVersion.Platform == PlatformID.Unix;

        private Color background_color = Color.Transparent;

        public delegate void LinesDrawnHandler(Graphics g, bool only_static_overlays);
        public event LinesDrawnHandler LinesDrawn;

        public delegate void BackgroundClearedHandler(Graphics g, Rectangle bounds);
        public event BackgroundClearedHandler BackgroundCleared;

        public event EventHandler<EventArgs> AddLegend;
        public event EventHandler<EventArgs> AutoRemoveLegend;

        public PlotSurface2D()
        {
            this.__ps = new NPlot.PlotSurface2D();
            this.CanFocus = false;
            SetSizeRequest(200, 150);

            if (Environment.OSVersion.Platform == PlatformID.Unix &&
                Environment.OSVersion.Version.Major >= 10)
            {
                // its MacOSX -> no working implementation for Gtk.DotNet.Graphics.FromDrawable() available
                this.no_gtk_dotnet = true;
            }
        }

        public void Refresh()
        {
            this.UpdateCache();
            this.QueueDrawArea();
        }

        internal void QueueDrawArea()
        {
            lock (this)
                this.QueueDrawArea(0, 0, this.Allocation.Width, this.Allocation.Height);
        }

        internal void UpdateCache()
        {
            this.UpdateCache(false);
        }

        internal void UpdateCache(bool only_overlays)
        {
            if (this.is_allocated == false)
                return;

            Bitmap bmp;

            lock (this)
            {
                if (only_overlays && this.bitmap_without_overlays_cache != null)
                {
                    bmp = (Bitmap)this.bitmap_without_overlays_cache.Clone();
                }
                else
                {
                    bmp = new Bitmap(this.current_allocation.Width, this.current_allocation.Height);
                    only_overlays = false;
                }

                using (Graphics g = Graphics.FromImage(bmp))
                {
                    Rectangle bounds = new Rectangle(0, 0, this.current_allocation.Width, this.current_allocation.Height);

                    try
                    {
                        if (only_overlays == false)
                            g.Clear(this.background_color);

                        this.BackgroundCleared?.Invoke(g, bounds);

                        if (only_overlays == false && this.__AxesAreValid)
                            this.Draw(g, bounds);

                        // FIXME: this is just a workaround for bug #33964 and should be removed as soon as possible
                        // for some reason Bitmap.Clone() returns an empty image if data is not accessed at least once
                        // FIXME: removed this fix since even more problems occured with the new version of libgdiplus (>=6.0)
                        // see #34292 and #32805. The better solution is to define a dependency to libgdiplus <<6.0 and provide
                        // the package in our package source
                        // The problem returns with libgdiplus 6.0.5
                        // The memory leak was fixed, but not this problem (see ticket #36213)
                        if (this.is_unix && only_overlays == false)
                        {
                            using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
                                bmp.Save(ms, System.Drawing.Imaging.ImageFormat.MemoryBmp);
                        }

                        this.bitmap_without_overlays_cache?.Dispose();
                        this.bitmap_without_overlays_cache = (Bitmap)bmp.Clone();

                        if (this.PhysicalXAxis1Cache == null || this.PhysicalYAxis1Cache == null || this.__AxesAreValid == false)
                        {
                            this.bitmap_cache?.Dispose();
                            this.bitmap_cache = (Bitmap)bmp;
                            return;
                        }

                        this.LinesDrawn?.Invoke(g, only_overlays == false);

                        this.DrawLegend(g);

                        this.bitmap_cache?.Dispose();
                        this.bitmap_cache = (Bitmap)bmp;
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("Exception in PlotSurface2D.UpdateCache(): " + ex);
                    }
                }
            }

            // force GC to collect garbage every ten updates to conserve memory
            if (++this.update_counter == 10)
            {
                this.update_counter = 0;
                GC.Collect();
            }
        }

        public void DrawLegend(Graphics g)
        {
            if (this.Legend != null && this.__ps.LegendInfo != null)
                this.Legend.Draw(g,
                                 this.__ps.LegendInfo.Position,
                                 this.__ps.LegendInfo.Drawables,
                                 this.__ps.LegendInfo.Scale);
        }

        protected override bool OnExposeEvent(Gdk.EventExpose evnt)
        {
            lock (this)
            {
                if (this.bitmap_cache == null)
                    return true;

                Gdk.Rectangle area = evnt.Area;

                if (this.no_gtk_dotnet)
                {
                    using (Gdk.GC gc = new Gdk.GC(evnt.Window))
                    using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
                    {
                        this.bitmap_cache.Save(stream, System.Drawing.Imaging.ImageFormat.MemoryBmp);
                        using (Gdk.Pixbuf pixbuf = new Gdk.Pixbuf(stream.ToArray()))
                            evnt.Window.DrawPixbuf(gc, pixbuf, area.X, area.Y, area.X, area.Y, area.Width, area.Height,
                                                   Gdk.RgbDither.Normal, 0, 0);
                    }
                }
                else
                {
                    using (Graphics g = Gtk.DotNet.Graphics.FromDrawable(evnt.Window))
                    {
                        Rectangle bounds = new Rectangle(area.X, area.Y, area.Width, area.Height);
                        g.DrawImage(bitmap_cache, bounds, bounds, GraphicsUnit.Pixel);
                    }
                }
            }
            return true;
        }

        protected override void OnSizeAllocated(Gdk.Rectangle allocation)
        {
            lock (this)
            {
                this.is_allocated = true;
                this.current_allocation = allocation;

                this.CheckLegendSize(allocation);
                this.UpdateCache();
                base.OnSizeAllocated(allocation);
            }
        }

        public void CheckLegendSize()
        {
            this.CheckLegendSize(this.Allocation);
        }

        private void CheckLegendSize(Gdk.Rectangle allocation)
        {
            if (this.Drawables.Count == 0)
                return;

            lock (this)
            {
                if (this.Legend == null && this.AddLegend != null)
                    this.AddLegend(this, EventArgs.Empty);

                if (this.Legend == null)
                    return;

                if (this.EstimatedLegendHeight(this.Legend.Font) > allocation.Height)
                {
                    if (this.Legend != null)
                    {
                        Console.WriteLine("Disable legend");
                        this.Legend = null;
                        this.AutoRemoveLegend?.Invoke(this, EventArgs.Empty);
                    }
                    return;
                }
            }

            return;
        }

        public virtual void Draw(Graphics g, Rectangle bounds)
        {
            lock (this)
                this.__ps.Draw(g, bounds);
        }

        public double EstimatedLegendHeight(Font legend_font)
        {
            SizeF size = this.graphics_helper.MeasureString("abcABC", legend_font);
            return size.Height * this.Drawables.Count * 1.6;
        }

        public Gdk.Color BackgroundColor
        {
            get
            {
                lock (this)
                    return new Gdk.Color(this.background_color.R,
                                         this.background_color.G,
                                         this.background_color.B);
            }
            set
            {
                lock (this)
                    this.background_color = Color.FromArgb(value.Red   >> 8,
                                                           value.Green >> 8,
                                                           value.Blue  >> 8);
            }
        }

        #region IPlotSurface2D interface implementation

        public void Add(IDrawable p, int zOrder)
        {
            lock (this)
                this.__ps.Add(p, zOrder);
        }

        public void Add(IDrawable p)
        {
            lock (this)
                this.__ps.Add(p);
        }

        public void Add(IDrawable p, NPlot.PlotSurface2D.XAxisPosition xp, NPlot.PlotSurface2D.YAxisPosition yp)
        {
            lock (this)
                this.__ps.Add(p, xp, yp);
        }

        public void Add(NPlot.IDrawable p, NPlot.PlotSurface2D.XAxisPosition xp, NPlot.PlotSurface2D.YAxisPosition yp, int zOrder)
        {
            lock (this)
                this.__ps.Add(p, xp, yp, zOrder);
        }

        public void Clear()
        {
            lock (this)
                this.__ps.Clear();
        }

        public Legend Legend
        {
            get
            {
                lock (this)
                    return this.__ps.Legend;
            }
            set
            {
                lock (this)
                {
                    this.__ps.Legend = value;
                }
            }
        }

        public int Padding
        {
            get
            {
                lock (this)
                    return this.__ps.Padding;
            }
            set
            {
                lock (this)
                    this.__ps.Padding = value;
            }
        }

        public int LegendZOrder
        {
            get
            {
                lock (this)
                    return this.__ps.LegendZOrder;
            }
            set
            {
                lock (this)
                    this.__ps.LegendZOrder = value;
            }
        }

        public Color PlotBackColor
        {
            set
            {
                lock (this)
                    this.__ps.PlotBackColor = value;
            }
        }

        public Bitmap PlotBackImage
        {
            set
            {
                lock (this)
                    this.__ps.PlotBackImage = value;
            }
        }

        public IRectangleBrush PlotBackBrush
        {
            set
            {
                lock (this)
                    this.__ps.PlotBackBrush = value;
            }
        }

        public Color TitleColor
        {
            set
            {
                lock (this)
                    this.__ps.TitleColor = value;
            }
        }

        public Brush TitleBrush
        {
            get
            {
                lock (this)
                    return this.__ps.TitleBrush;
            }
            set
            {
                lock (this)
                    this.__ps.TitleBrush = value;
            }
        }

        public ArrayList Drawables
        {
            get
            {
                lock (this)
                    return this.__ps.Drawables;
            }
        }

        public ArrayList XAxisPositions
        {
            get
            {
                lock (this)
                    return this.__ps.XAxisPositions;
            }
        }

        public ArrayList YAxisPositions
        {
            get
            {
                lock (this)
                    return this.__ps.YAxisPositions;
            }
        }

        public void Remove(IDrawable p, bool updateAxes)
        {
            lock (this)
                this.__ps.Remove(p, updateAxes);
        }

        public string Title
        {
            get
            {
                lock (this)
                    return this.__ps.Title;
            }
            set
            {
                lock (this)
                    this.__ps.Title = value;
            }
        }

        public Font TitleFont
        {
            get
            {
                lock (this)
                    return this.__ps.TitleFont;
            }
            set
            {
                lock (this)
                    this.__ps.TitleFont = value;
            }
        }

        public System.Drawing.Drawing2D.SmoothingMode SmoothingMode
        {
            get
            {
                lock (this)
                    return this.__ps.SmoothingMode;
            }
            set
            {
                lock (this)
                    this.__ps.SmoothingMode = value;
            }
        }

        public void AddAxesConstraint(AxesConstraint c)
        {
            lock (this)
                this.__ps.AddAxesConstraint(c);
        }

        public Axis XAxis1
        {
            get
            {
                lock (this)
                    return this.__ps.XAxis1;
            }
            set
            {
                lock (this)
                    this.__ps.XAxis1 = value;
            }
        }

        public Axis YAxis1
        {
            get
            {
                lock (this)
                    return this.__ps.YAxis1;
            }
            set
            {
                lock (this)
                    this.__ps.YAxis1 = value;
            }
        }

        public Axis XAxis2
        {
            get
            {
                lock (this)
                    return this.__ps.XAxis2;
            }
            set
            {
                lock (this)
                    this.__ps.XAxis2 = value;
            }
        }

        public Axis YAxis2
        {
            get
            {
                lock (this)
                    return this.__ps.YAxis2;
            }
            set
            {
                lock (this)
                    this.__ps.YAxis2 = value;
            }
        }

        public bool AutoScaleTitle
        {
            get
            {
                lock (this)
                    return this.__ps.AutoScaleTitle;
            }
            set
            {
                lock (this)
                    this.__ps.AutoScaleTitle = value;
            }
        }

        public bool AutoScaleAutoGeneratedAxes
        {
            get
            {
                lock (this)
                    return this.__ps.AutoScaleAutoGeneratedAxes;
            }
            set
            {
                lock (this)
                    this.__ps.AutoScaleAutoGeneratedAxes = value;
            }
        }

        public Bitmap Bitmap
        {
            get
            {
                lock (this)
                    return this.bitmap_cache;
            }
        }

        public int Width
        {
            get
            {
                lock (this)
                    return this.current_allocation.Width;
            }
        }

        public int Height
        {
            get
            {
                lock (this)
                    return this.current_allocation.Height;
            }
        }

        public PhysicalAxis PhysicalXAxis1Cache
        {
            get
            {
                lock (this)
                    return this.__ps.PhysicalXAxis1Cache;
            }
        }

        public PhysicalAxis PhysicalYAxis1Cache
        {
            get
            {
                lock (this)
                    return this.__ps.PhysicalYAxis1Cache;
            }
        }

        #endregion

        private bool __AxesAreValid
        {
            get
            {
                if (this.XAxis1 == null || this.YAxis1 == null)
                    return false;

                if (double.IsNaN(this.XAxis1.WorldMax) ||
                    double.IsNaN(this.XAxis1.WorldMin) ||
                    double.IsNaN(this.YAxis1.WorldMax) ||
                    double.IsNaN(this.YAxis1.WorldMin))
                    return false;

                return true;
            }
        }

        private bool __Axes2AreValid
        {
            get
            {
                if (this.XAxis2 == null || this.YAxis2 == null)
                    return true;

                if (double.IsNaN(this.XAxis2.WorldMax) ||
                    double.IsNaN(this.XAxis2.WorldMin) ||
                    double.IsNaN(this.YAxis2.WorldMax) ||
                    double.IsNaN(this.YAxis2.WorldMin))
                {
                    return false;
                }

                return true;
            }
        }
    }
}
