// --------------------------------------------------------------------------------------------------------------------
// <copyright file="CanvasRenderContext.cs" company="OxyPlot">
//   Copyright (c) 2014 OxyPlot contributors
// </copyright>
// <summary>
//   Provides a render context for Android.Graphics.Canvas.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace OxyPlot.Xamarin.Android
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using global::Android.Graphics;

    /// <summary>
    /// Provides a render context for Android.Graphics.Canvas.
    /// </summary>
    public class CanvasRenderContext : RenderContextBase
    {
        /// <summary>
        /// The images in use
        /// </summary>
        private readonly HashSet<OxyImage> imagesInUse = new HashSet<OxyImage>();

        /// <summary>
        /// The image cache
        /// </summary>
        private readonly Dictionary<OxyImage, Bitmap> imageCache = new Dictionary<OxyImage, Bitmap>();

        /// <summary>
        /// The current paint.
        /// </summary>
        private readonly Paint paint;

        /// <summary>
        /// A reusable path object.
        /// </summary>
        private readonly Path path;

        /// <summary>
        /// A reusable bounds rectangle.
        /// </summary>
        private readonly Rect bounds;

        /// <summary>
        /// A reusable list of points.
        /// </summary>
        private readonly List<float> pts;

        /// <summary>
        /// The canvas to draw on.
        /// </summary>
        private Canvas canvas;

        /// <summary>
        /// Initializes a new instance of the <see cref="CanvasRenderContext" /> class.
        /// </summary>
        /// <param name="scale">The scale.</param>
        public CanvasRenderContext(double scale)
        {
            this.paint = new Paint();
            this.path = new Path();
            this.bounds = new Rect();
            this.pts = new List<float>();
            this.Scale = scale;
        }

        /// <summary>
        /// Gets the factor that this.Scales from OxyPlot´s device independent pixels (96 dpi) to 
        /// Android´s density-independent pixels (160 dpi).
        /// </summary>
        /// <remarks>See <a href="http://developer.android.com/guide/practices/screens_support.html">Supporting multiple screens.</a>.</remarks>
        public double Scale { get; private set; }

        /// <summary>
        /// Sets the target.
        /// </summary>
        /// <param name="c">The canvas.</param>
        public void SetTarget(Canvas c)
        {
            this.canvas = c;
        }

        /// <summary>
        /// Draws an ellipse.
        /// </summary>
        /// <param name="rect">The rectangle.</param>
        /// <param name="fill">The fill color.</param>
        /// <param name="stroke">The stroke color.</param>
        /// <param name="thickness">The thickness.</param>
        public override void DrawEllipse(OxyRect rect, OxyColor fill, OxyColor stroke, double thickness)
        {
            this.paint.Reset();
            {
                if (fill.IsVisible())
                {
                    this.SetFill(fill);
                    this.canvas.DrawOval(this.Convert(rect), this.paint);
                }

                if (stroke.IsVisible())
                {
                    this.SetStroke(stroke, thickness);
                    this.canvas.DrawOval(this.Convert(rect), this.paint);
                }
            }
        }

        /// <summary>
        /// Draws the collection of ellipses, where all have the same stroke and fill.
        /// This performs better than calling DrawEllipse multiple times.
        /// </summary>
        /// <param name="rectangles">The rectangles.</param>
        /// <param name="fill">The fill color.</param>
        /// <param name="stroke">The stroke color.</param>
        /// <param name="thickness">The stroke thickness.</param>
        public override void DrawEllipses(IList<OxyRect> rectangles, OxyColor fill, OxyColor stroke, double thickness)
        {
            this.paint.Reset();
            {
                foreach (var rect in rectangles)
                {
                    if (fill.IsVisible())
                    {
                        this.SetFill(fill);
                        this.canvas.DrawOval(this.Convert(rect), this.paint);
                    }

                    if (stroke.IsVisible())
                    {
                        this.SetStroke(stroke, thickness);
                        this.canvas.DrawOval(this.Convert(rect), this.paint);
                    }
                }
            }
        }

        /// <summary>
        /// Draws a polyline.
        /// </summary>
        /// <param name="points">The points.</param>
        /// <param name="stroke">The stroke color.</param>
        /// <param name="thickness">The stroke thickness.</param>
        /// <param name="dashArray">The dash array.</param>
        /// <param name="lineJoin">The line join type.</param>
        /// <param name="aliased">if set to <c>true</c> the shape will be aliased.</param>
        public override void DrawLine(IList<ScreenPoint> points, OxyColor stroke, double thickness, double[] dashArray, LineJoin lineJoin, bool aliased)
        {
            this.paint.Reset();
            {
                this.path.Reset();
                {
                    this.SetPath(points, aliased);
                    this.SetStroke(stroke, thickness, dashArray, lineJoin, aliased);
                    this.canvas.DrawPath(this.path, this.paint);
                }
            }
        }

        /// <summary>
        /// Draws multiple line segments defined by points (0,1) (2,3) (4,5) etc.
        /// This should have better performance than calling DrawLine for each segment.
        /// </summary>
        /// <param name="points">The points.</param>
        /// <param name="stroke">The stroke color.</param>
        /// <param name="thickness">The stroke thickness.</param>
        /// <param name="dashArray">The dash array.</param>
        /// <param name="lineJoin">The line join type.</param>
        /// <param name="aliased">If set to <c>true</c> the shape will be aliased.</param>
        public override void DrawLineSegments(IList<ScreenPoint> points, OxyColor stroke, double thickness, double[] dashArray, LineJoin lineJoin, bool aliased)
        {
            this.paint.Reset();
            {
                this.SetStroke(stroke, thickness, dashArray, lineJoin, aliased);
                this.pts.Clear();
                if (aliased)
                {
                    foreach (var p in points)
                    {
                        this.pts.Add(this.ConvertAliased(p.X));
                        this.pts.Add(this.ConvertAliased(p.Y));
                    }
                }
                else
                {
                    foreach (var p in points)
                    {
                        this.pts.Add(this.Convert(p.X));
                        this.pts.Add(this.Convert(p.Y));
                    }
                }

                this.canvas.DrawLines(this.pts.ToArray(), this.paint);
            }
        }

        /// <summary>
        /// Draws a polygon. The polygon can have stroke and/or fill.
        /// </summary>
        /// <param name="points">The points.</param>
        /// <param name="fill">The fill color.</param>
        /// <param name="stroke">The stroke color.</param>
        /// <param name="thickness">The stroke thickness.</param>
        /// <param name="dashArray">The dash array.</param>
        /// <param name="lineJoin">The line join type.</param>
        /// <param name="aliased">If set to <c>true</c> the shape will be aliased.</param>
        public override void DrawPolygon(IList<ScreenPoint> points, OxyColor fill, OxyColor stroke, double thickness, double[] dashArray, LineJoin lineJoin, bool aliased)
        {
            this.paint.Reset();
            {
                this.path.Reset();
                {
                    this.SetPath(points, aliased);
                    this.path.Close();

                    if (fill.IsVisible())
                    {
                        this.SetFill(fill);
                        this.canvas.DrawPath(this.path, this.paint);
                    }

                    if (stroke.IsVisible())
                    {
                        this.SetStroke(stroke, thickness, dashArray, lineJoin, aliased);
                        this.canvas.DrawPath(this.path, this.paint);
                    }
                }
            }
        }

        /// <summary>
        /// Draws a rectangle.
        /// </summary>
        /// <param name="rect">The rectangle.</param>
        /// <param name="fill">The fill color.</param>
        /// <param name="stroke">The stroke color.</param>
        /// <param name="thickness">The stroke thickness.</param>
        public override void DrawRectangle(OxyRect rect, OxyColor fill, OxyColor stroke, double thickness)
        {
            this.paint.Reset();
            {
                if (fill.IsVisible())
                {
                    this.SetFill(fill);
                    this.canvas.DrawRect(this.ConvertAliased(rect.Left), this.ConvertAliased(rect.Top), this.ConvertAliased(rect.Right), this.ConvertAliased(rect.Bottom), this.paint);
                }

                if (stroke.IsVisible())
                {
                    this.SetStroke(stroke, thickness, aliased: true);
                    this.canvas.DrawRect(this.ConvertAliased(rect.Left), this.ConvertAliased(rect.Top), this.ConvertAliased(rect.Right), this.ConvertAliased(rect.Bottom), this.paint);
                }
            }
        }

        /// <summary>
        /// Draws the text.
        /// </summary>
        /// <param name="p">The position of the text.</param>
        /// <param name="text">The text.</param>
        /// <param name="fill">The fill color.</param>
        /// <param name="fontFamily">The font family.</param>
        /// <param name="fontSize">Size of the font.</param>
        /// <param name="fontWeight">The font weight.</param>
        /// <param name="rotate">The rotation angle.</param>
        /// <param name="halign">The horizontal alignment.</param>
        /// <param name="valign">The vertical alignment.</param>
        /// <param name="maxSize">The maximum size of the text.</param>
        public override void DrawText(ScreenPoint p, string text, OxyColor fill, string fontFamily, double fontSize, double fontWeight, double rotate, HorizontalAlignment halign, VerticalAlignment valign, OxySize? maxSize)
        {
            this.paint.Reset();
            {
                this.paint.TextSize = this.Convert(fontSize);
                this.SetFill(fill);

                float width;
                float height;
                float lineHeight, delta;
                this.GetFontMetrics(this.paint, out lineHeight, out delta);
                if (maxSize.HasValue || halign != HorizontalAlignment.Left || valign != VerticalAlignment.Bottom)
                {
                    this.paint.GetTextBounds(text, 0, text.Length, this.bounds);
                    width = this.bounds.Left + this.bounds.Width();
                    height = lineHeight;
                }
                else
                {
                    width = height = 0f;
                }

                if (maxSize.HasValue)
                {
                    var maxWidth = this.Convert(maxSize.Value.Width);
                    var maxHeight = this.Convert(maxSize.Value.Height);

                    if (width > maxWidth)
                    {
                        width = maxWidth;
                    }

                    if (height > maxHeight)
                    {
                        height = maxHeight;
                    }
                }

                var dx = halign == HorizontalAlignment.Left ? 0d : (halign == HorizontalAlignment.Center ? -width * 0.5 : -width);
                var dy = valign == VerticalAlignment.Bottom ? 0d : (valign == VerticalAlignment.Middle ? height * 0.5 : height);
                var x0 = -this.bounds.Left;
                var y0 = delta;

                this.canvas.Save();
                this.canvas.Translate(this.Convert(p.X), this.Convert(p.Y));
                this.canvas.Rotate((float)rotate);
                this.canvas.Translate((float)dx + x0, (float)dy + y0);

                if (maxSize.HasValue)
                {
                    var x1 = -x0;
                    var y1 = -height - y0;
                    this.canvas.ClipRect(x1, y1, x1 + width, y1 + height);
                    this.canvas.Translate(0, lineHeight - height);
                }

                this.canvas.DrawText(text, 0, 0, this.paint);
                this.canvas.Restore();
            }
        }

        /// <summary>
        /// Measures the text.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <param name="fontFamily">The font family.</param>
        /// <param name="fontSize">Size of the font.</param>
        /// <param name="fontWeight">The font weight.</param>
        /// <returns>The text size.</returns>
        public override OxySize MeasureText(string text, string fontFamily, double fontSize, double fontWeight)
        {
            if (string.IsNullOrEmpty(text))
            {
                return OxySize.Empty;
            }

            this.paint.Reset();
            {
                this.paint.AntiAlias = true;
                this.paint.TextSize = this.Convert(fontSize);
                float lineHeight, delta;
                this.GetFontMetrics(this.paint, out lineHeight, out delta);
                this.paint.GetTextBounds(text, 0, text.Length, this.bounds);
                return new OxySize(this.bounds.Width() / this.Scale, lineHeight / this.Scale);
            }
        }

        /// <summary>
        /// Sets the clip rectangle.
        /// </summary>
        /// <param name="rect">The clip rectangle.</param>
        /// <returns>True if the clip rectangle was set.</returns>
        public override bool SetClip(OxyRect rect)
        {
            this.canvas.Save();
            this.canvas.ClipRect(this.Convert(rect));
            return true;
        }

        /// <summary>
        /// Resets the clip rectangle.
        /// </summary>
        public override void ResetClip()
        {
            this.canvas.Restore();
        }

        /// <summary>
        /// Draws the specified portion of the specified <see cref="OxyImage" /> at the specified location and with the specified size.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="srcX">The x-coordinate of the upper-left corner of the portion of the source image to draw.</param>
        /// <param name="srcY">The y-coordinate of the upper-left corner of the portion of the source image to draw.</param>
        /// <param name="srcWidth">Width of the portion of the source image to draw.</param>
        /// <param name="srcHeight">Height of the portion of the source image to draw.</param>
        /// <param name="destX">The x-coordinate of the upper-left corner of drawn image.</param>
        /// <param name="destY">The y-coordinate of the upper-left corner of drawn image.</param>
        /// <param name="destWidth">The width of the drawn image.</param>
        /// <param name="destHeight">The height of the drawn image.</param>
        /// <param name="opacity">The opacity.</param>
        /// <param name="interpolate">interpolate if set to <c>true</c>.</param>
        public override void DrawImage(
            OxyImage source,
            double srcX,
            double srcY,
            double srcWidth,
            double srcHeight,
            double destX,
            double destY,
            double destWidth,
            double destHeight,
            double opacity,
            bool interpolate)
        {
            var image = this.GetImage(source);
            if (image == null)
            {
                return;
            }

            var src = new Rect((int)srcX, (int)srcY, (int)(srcX + srcWidth), (int)(srcY + srcHeight));
            var dest = new RectF(this.Convert(destX), this.Convert(destY), this.Convert(destX + destWidth), this.Convert(destY + destHeight));

            this.paint.Reset();

            // TODO: support opacity
            this.canvas.DrawBitmap(image, src, dest, this.paint);
        }

        /// <summary>
        /// Cleans up resources not in use.
        /// </summary>
        /// <remarks>This method is called at the end of each rendering.</remarks>
        public override void CleanUp()
        {
            var imagesToRelease = this.imageCache.Keys.Where(i => !this.imagesInUse.Contains(i)).ToList();
            foreach (var i in imagesToRelease)
            {
                var image = this.GetImage(i);
                image.Dispose();
                this.imageCache.Remove(i);
            }

            this.imagesInUse.Clear();
        }

        /// <summary>
        /// Gets font metrics for the font in the specified paint.
        /// </summary>
        /// <param name="paint">The paint.</param>
        /// <param name="defaultLineHeight">Default line height.</param>
        /// <param name="delta">The vertical delta.</param>
        private void GetFontMetrics(Paint paint, out float defaultLineHeight, out float delta)
        {
            var metrics = paint.GetFontMetrics();
            var ascent = -metrics.Ascent;
            var descent = metrics.Descent;
            var leading = metrics.Leading;

            //// http://stackoverflow.com/questions/5511830/how-does-line-spacing-work-in-core-text-and-why-is-it-different-from-nslayoutm

            leading = leading < 0 ? 0 : (float)Math.Floor(leading + 0.5f);
            var lineHeight = (float)Math.Floor(ascent + 0.5f) + (float)Math.Floor(descent + 0.5) + leading;
            var ascenderDelta = leading >= 0 ? 0 : (float)Math.Floor((0.2 * lineHeight) + 0.5);
            defaultLineHeight = lineHeight + ascenderDelta;
            delta = ascenderDelta - descent;
        }

        /// <summary>
        /// Converts the specified coordinate to a scaled coordinate.
        /// </summary>
        /// <param name="x">The coordinate to convert.</param>
        /// <returns>The converted coordinate.</returns>
        private float Convert(double x)
        {
            return (float)(x * this.Scale);
        }

        /// <summary>
        /// Converts the specified rectangle to a scaled rectangle.
        /// </summary>
        /// <param name="rect">The rectangle to convert.</param>
        /// <returns>The converted rectangle.</returns>
        private RectF Convert(OxyRect rect)
        {
            return new RectF(this.ConvertAliased(rect.Left), this.ConvertAliased(rect.Top), this.ConvertAliased(rect.Right), this.ConvertAliased(rect.Bottom));
        }

        /// <summary>
        /// Converts the specified coordinate to a pixel-aligned scaled coordinate.
        /// </summary>
        /// <param name="x">The coordinate to convert.</param>
        /// <returns>The converted coordinate.</returns>
        private float ConvertAliased(double x)
        {
            return (int)(x * this.Scale) + 0.5f;
        }

        /// <summary>
        /// Sets the path to the specified points.
        /// </summary>
        /// <param name="points">The points defining the path.</param>
        /// <param name="aliased">If set to <c>true</c> aliased.</param>
        private void SetPath(IList<ScreenPoint> points, bool aliased)
        {
            if (aliased)
            {
                this.path.MoveTo(this.ConvertAliased(points[0].X), this.ConvertAliased(points[0].Y));
                for (int i = 1; i < points.Count; i++)
                {
                    this.path.LineTo(this.ConvertAliased(points[i].X), this.ConvertAliased(points[i].Y));
                }
            }
            else
            {
                this.path.MoveTo(this.Convert(points[0].X), this.Convert(points[0].Y));
                for (int i = 1; i < points.Count; i++)
                {
                    this.path.LineTo(this.Convert(points[i].X), this.Convert(points[i].Y));
                }
            }
        }

        /// <summary>
        /// Sets the fill style.
        /// </summary>
        /// <param name="fill">The fill color.</param>
        private void SetFill(OxyColor fill)
        {
            this.paint.SetStyle(Paint.Style.Fill);
            this.paint.Color = fill.ToColor();
            this.paint.AntiAlias = true;
        }

        /// <summary>
        /// Sets the stroke style.
        /// </summary>
        /// <param name="stroke">The stroke color.</param>
        /// <param name="thickness">The stroke thickness.</param>
        /// <param name="dashArray">The dash array.</param>
        /// <param name="lineJoin">The line join.</param>
        /// <param name="aliased">Use aliased strokes if set to <c>true</c>.</param>
        private void SetStroke(OxyColor stroke, double thickness, double[] dashArray = null, LineJoin lineJoin = LineJoin.Miter, bool aliased = false)
        {
            this.paint.SetStyle(Paint.Style.Stroke);
            this.paint.Color = stroke.ToColor();
            this.paint.StrokeWidth = this.Convert(thickness);
            this.paint.StrokeJoin = lineJoin.Convert();
            if (dashArray != null)
            {
                var dashArrayF = dashArray.Select(this.Convert).ToArray();
                this.paint.SetPathEffect(new DashPathEffect(dashArrayF, 0f));
            }

            this.paint.AntiAlias = !aliased;
        }

        /// <summary>
        /// Gets the image from cache or creates a new <see cref="Bitmap" />.
        /// </summary>
        /// <param name="source">The source image.</param>
        /// <returns>A <see cref="Bitmap" />.</returns>
        private Bitmap GetImage(OxyImage source)
        {
            if (source == null)
            {
                return null;
            }

            if (!this.imagesInUse.Contains(source))
            {
                this.imagesInUse.Add(source);
            }

            Bitmap bitmap;
            if (!this.imageCache.TryGetValue(source, out bitmap))
            {
                var bytes = source.GetData();
                bitmap = BitmapFactory.DecodeByteArray(bytes, 0, bytes.Length);
                this.imageCache.Add(source, bitmap);
            }

            return bitmap;
        }
    }
}