// 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.ComponentModel;
using System.Data;
using System.Drawing;
using Gtk;

using NPlot;


namespace Hiperscan.GUI.NPlot_Gtk
{

	public class PlotSurface2D : DrawingArea, IPlotSurface2D
	{
		private NPlot.PlotSurface2D __ps;
		
		private Bitmap bitmap_cache;
		private Bitmap bitmap_without_overlays_cache;
		private Gdk.Rectangle current_allocation;
		private Graphics graphics_helper = Graphics.FromImage(new Bitmap(100, 100));
		
		private volatile bool is_allocated = false;
		private volatile int update_counter = 0;
		private volatile bool no_gtk_dotnet = false;

		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)
				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)
							g.Clear(this.background_color);
					
						if (this.BackgroundCleared != null)
							this.BackgroundCleared(g, bounds);
						
						if (!only_overlays && this.__AxesAreValid)
							this.Draw(g, bounds);
						
						if (this.bitmap_without_overlays_cache != null)
							this.bitmap_without_overlays_cache.Dispose();
						this.bitmap_without_overlays_cache = (Bitmap)bmp.Clone();
						
						if (this.PhysicalXAxis1Cache == null || this.PhysicalYAxis1Cache == null || !this.__AxesAreValid)
						{
							this.bitmap_cache = bmp;
							return;
						}

						if (this.LinesDrawn != null)
							this.LinesDrawn(g, !only_overlays);

						this.DrawLegend(g);
					
						if (this.bitmap_cache != null)
							this.bitmap_cache.Dispose();
						this.bitmap_cache = bmp;
					}
					catch (System.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 args)
		{
			lock (this)
			{
				if (this.bitmap_cache == null)
					return true;
				
				Gdk.Rectangle area = args.Area;

				if (this.no_gtk_dotnet)
				{
					using (Gdk.GC gc = new Gdk.GC(args.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()))
								args.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(args.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;
						if (this.AutoRemoveLegend != null)
							this.AutoRemoveLegend(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 System.Drawing.Color PlotBackColor
		{
			set
			{
				lock (this)
					this.__ps.PlotBackColor = value;
			}
		}
		
		public System.Drawing.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 System.Drawing.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;
			}
		}
	}
}
