#region header
// ========================================================================
// Copyright (c) 2018 - Julien Caillon (julien.caillon@gmail.com)
// This file (GraphicsAdapter.cs) is part of YamuiFramework.
// 
// YamuiFramework is a 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.
// 
// YamuiFramework 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 YamuiFramework. If not, see <http://www.gnu.org/licenses/>.
// ========================================================================
#endregion
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using YamuiFramework.HtmlRenderer.Core.Adapters;
using YamuiFramework.HtmlRenderer.Core.Adapters.Entities;
using YamuiFramework.HtmlRenderer.Core.Core.Utils;
using YamuiFramework.HtmlRenderer.WinForms.Utilities;

namespace YamuiFramework.HtmlRenderer.WinForms.Adapters {
    /// <summary>
    /// Adapter for WinForms Graphics for core.
    /// </summary>
    internal sealed class GraphicsAdapter : RGraphics {
        #region Fields and Consts

        /// <summary>
        /// used for <see cref="MeasureString(string,RFont,double,out int,out double)"/> calculation.
        /// </summary>
        private static readonly int[] _charFit = new int[1];

        /// <summary>
        /// used for <see cref="MeasureString(string,RFont,double,out int,out double)"/> calculation.
        /// </summary>
        private static readonly int[] _charFitWidth = new int[1000];

        /// <summary>
        /// Used for GDI+ measure string.
        /// </summary>
        private static readonly CharacterRange[] _characterRanges = new CharacterRange[1];

        /// <summary>
        /// The string format to use for measuring strings for GDI+ text rendering
        /// </summary>
        private static readonly StringFormat _stringFormat;

        /// <summary>
        /// The string format to use for rendering strings for GDI+ text rendering
        /// </summary>
        private static readonly StringFormat _stringFormat2;

        /// <summary>
        /// The wrapped WinForms graphics object
        /// </summary>
        private readonly Graphics _g;

        /// <summary>
        /// Use GDI+ text rendering to measure/draw text.
        /// </summary>
        private readonly bool _useGdiPlusTextRendering;

#if !MONO
        /// <summary>
        /// the initialized HDC used
        /// </summary>
        private IntPtr _hdc;
#endif

        /// <summary>
        /// if to release the graphics object on dispose
        /// </summary>
        private readonly bool _releaseGraphics;

        /// <summary>
        /// If text alignment was set to RTL
        /// </summary>
        private bool _setRtl;

        #endregion

        /// <summary>
        /// Init static resources.
        /// </summary>
        static GraphicsAdapter() {
            _stringFormat = new StringFormat(StringFormat.GenericTypographic);
            _stringFormat.FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.MeasureTrailingSpaces;

            _stringFormat2 = new StringFormat(StringFormat.GenericTypographic);
        }

        /// <summary>
        /// Init.
        /// </summary>
        /// <param name="g">the win forms graphics object to use</param>
        /// <param name="useGdiPlusTextRendering">Use GDI+ text rendering to measure/draw text</param>
        /// <param name="releaseGraphics">optional: if to release the graphics object on dispose (default - false)</param>
        public GraphicsAdapter(Graphics g, bool useGdiPlusTextRendering, bool releaseGraphics = false)
            : base(WinFormsAdapter.Instance, Utils.Convert(g.ClipBounds)) {
            ArgChecker.AssertArgNotNull(g, "g");

            _g = g;
            _releaseGraphics = releaseGraphics;

#if MONO
            _useGdiPlusTextRendering = true;
#else
            _useGdiPlusTextRendering = useGdiPlusTextRendering;
#endif
        }

        public override void PopClip() {
            ReleaseHdc();
            _clipStack.Pop();
            _g.SetClip(Utils.Convert(_clipStack.Peek()), CombineMode.Replace);
        }

        public override void PushClip(RRect rect) {
            ReleaseHdc();
            _clipStack.Push(rect);
            _g.SetClip(Utils.Convert(rect), CombineMode.Replace);
        }

        public override void PushClipExclude(RRect rect) {
            ReleaseHdc();
            _clipStack.Push(_clipStack.Peek());
            _g.SetClip(Utils.Convert(rect), CombineMode.Exclude);
        }

        public override Object SetAntiAliasSmoothingMode() {
            ReleaseHdc();
            var prevMode = _g.SmoothingMode;
            _g.SmoothingMode = SmoothingMode.AntiAlias;
            return prevMode;
        }

        public override void ReturnPreviousSmoothingMode(Object prevMode) {
            if (prevMode != null) {
                ReleaseHdc();
                _g.SmoothingMode = (SmoothingMode) prevMode;
            }
        }

        public override RSize MeasureString(string str, RFont font) {
            if (_useGdiPlusTextRendering) {
                ReleaseHdc();
                var fontAdapter = (FontAdapter) font;
                var realFont = fontAdapter.Font;
                _characterRanges[0] = new CharacterRange(0, str.Length);
                _stringFormat.SetMeasurableCharacterRanges(_characterRanges);
                var size = _g.MeasureCharacterRanges(str, realFont, RectangleF.Empty, _stringFormat)[0].GetBounds(_g).Size;

                if (font.Height < 0) {
                    var height = realFont.Height;
                    var descent = realFont.Size*realFont.FontFamily.GetCellDescent(realFont.Style)/realFont.FontFamily.GetEmHeight(realFont.Style);
                    fontAdapter.SetMetrics(height, (int) Math.Round((height - descent + .5f)));
                }

                return Utils.Convert(size);
            } else {
#if !MONO
                SetFont(font);
                var size = new Size();
                Win32Utils.GetTextExtentPoint32(_hdc, str, str.Length, ref size);

                if (font.Height < 0) {
                    TextMetric lptm;
                    Win32Utils.GetTextMetrics(_hdc, out lptm);
                    ((FontAdapter) font).SetMetrics(size.Height, lptm.tmHeight - lptm.tmDescent + lptm.tmUnderlined + 1);
                }

                return Utils.Convert(size);
#else
                throw new InvalidProgramException("Invalid Mono code");
#endif
            }
        }

        public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) {
            charFit = 0;
            charFitWidth = 0;
            if (_useGdiPlusTextRendering) {
                ReleaseHdc();

                var size = MeasureString(str, font);

                for (int i = 1; i <= str.Length; i++) {
                    charFit = i - 1;
                    RSize pSize = MeasureString(str.Substring(0, i), font);
                    if (pSize.Height <= size.Height && pSize.Width < maxWidth)
                        charFitWidth = pSize.Width;
                    else
                        break;
                }
            } else {
#if !MONO
                SetFont(font);

                var size = new Size();
                Win32Utils.GetTextExtentExPoint(_hdc, str, str.Length, (int) Math.Round(maxWidth), _charFit, _charFitWidth, ref size);
                charFit = _charFit[0];
                charFitWidth = charFit > 0 ? _charFitWidth[charFit - 1] : 0;
#endif
            }
        }

        public override void DrawString(string str, RFont font, RColor color, RPoint point, RSize size, bool rtl) {
            if (_useGdiPlusTextRendering) {
                ReleaseHdc();
                SetRtlAlignGdiPlus(rtl);
                var brush = ((BrushAdapter) _adapter.GetSolidBrush(color)).Brush;
                _g.DrawString(str, ((FontAdapter) font).Font, brush, (int) (Math.Round(point.X) + (rtl ? size.Width : 0)), (int) Math.Round(point.Y), _stringFormat2);
            } else {
#if !MONO
                var pointConv = Utils.ConvertRound(point);
                var colorConv = Utils.Convert(color);

                if (color.A == 255) {
                    SetFont(font);
                    SetTextColor(colorConv);
                    SetRtlAlignGdi(rtl);

                    Win32Utils.TextOut(_hdc, pointConv.X, pointConv.Y, str, str.Length);
                } else {
                    InitHdc();
                    SetRtlAlignGdi(rtl);
                    DrawTransparentText(_hdc, str, font, pointConv, Utils.ConvertRound(size), colorConv);
                }
#endif
            }
        }

        public override RBrush GetTextureBrush(RImage image, RRect dstRect, RPoint translateTransformLocation) {
            var brush = new TextureBrush(((ImageAdapter) image).Image, Utils.Convert(dstRect));
            brush.TranslateTransform((float) translateTransformLocation.X, (float) translateTransformLocation.Y);
            return new BrushAdapter(brush, true);
        }

        public override RGraphicsPath GetGraphicsPath() {
            return new GraphicsPathAdapter();
        }

        public override void Dispose() {
            ReleaseHdc();
            if (_releaseGraphics)
                _g.Dispose();

            if (_useGdiPlusTextRendering && _setRtl)
                _stringFormat2.FormatFlags ^= StringFormatFlags.DirectionRightToLeft;
        }

        #region Delegate graphics methods

        public override void DrawLine(RPen pen, double x1, double y1, double x2, double y2) {
            ReleaseHdc();
            _g.DrawLine(((PenAdapter) pen).Pen, (float) x1, (float) y1, (float) x2, (float) y2);
        }

        public override void DrawRectangle(RPen pen, double x, double y, double width, double height) {
            ReleaseHdc();
            _g.DrawRectangle(((PenAdapter) pen).Pen, (float) x, (float) y, (float) width, (float) height);
        }

        public override void DrawRectangle(RBrush brush, double x, double y, double width, double height) {
            ReleaseHdc();
            _g.FillRectangle(((BrushAdapter) brush).Brush, (float) x, (float) y, (float) width, (float) height);
        }

        public override void DrawImage(RImage image, RRect destRect, RRect srcRect) {
            ReleaseHdc();
            _g.DrawImage(((ImageAdapter) image).Image, Utils.Convert(destRect), Utils.Convert(srcRect), GraphicsUnit.Pixel);
        }

        public override void DrawImage(RImage image, RRect destRect) {
            ReleaseHdc();
            _g.DrawImage(((ImageAdapter) image).Image, Utils.Convert(destRect));
        }

        public override void DrawPath(RPen pen, RGraphicsPath path) {
            _g.DrawPath(((PenAdapter) pen).Pen, ((GraphicsPathAdapter) path).GraphicsPath);
        }

        public override void DrawPath(RBrush brush, RGraphicsPath path) {
            ReleaseHdc();
            _g.FillPath(((BrushAdapter) brush).Brush, ((GraphicsPathAdapter) path).GraphicsPath);
        }

        public override void DrawPolygon(RBrush brush, RPoint[] points) {
            if (points != null && points.Length > 0) {
                ReleaseHdc();
                _g.FillPolygon(((BrushAdapter) brush).Brush, Utils.Convert(points));
            }
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Release current HDC to be able to use <see cref="Graphics"/> methods.
        /// </summary>
        private void ReleaseHdc() {
#if !MONO
            if (_hdc != IntPtr.Zero) {
                Win32Utils.SelectClipRgn(_hdc, IntPtr.Zero);
                _g.ReleaseHdc(_hdc);
                _hdc = IntPtr.Zero;
            }
#endif
        }

#if !MONO
        /// <summary>
        /// Init HDC for the current graphics object to be used to call GDI directly.
        /// </summary>
        private void InitHdc() {
            if (_hdc == IntPtr.Zero) {
                var clip = _g.Clip.GetHrgn(_g);

                _hdc = _g.GetHdc();
                _setRtl = false;
                Win32Utils.SetBkMode(_hdc, 1);

                Win32Utils.SelectClipRgn(_hdc, clip);

                Win32Utils.DeleteObject(clip);
            }
        }

        /// <summary>
        /// Set a resource (e.g. a font) for the specified device context.
        /// WARNING: Calling Font.ToHfont() many times without releasing the font handle crashes the app.
        /// </summary>
        private void SetFont(RFont font) {
            InitHdc();
            Win32Utils.SelectObject(_hdc, ((FontAdapter) font).HFont);
        }

        /// <summary>
        /// Set the text color of the device context.
        /// </summary>
        private void SetTextColor(Color color) {
            InitHdc();
            int rgb = (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R;
            Win32Utils.SetTextColor(_hdc, rgb);
        }

        /// <summary>
        /// Change text align to Left-to-Right or Right-to-Left if required.
        /// </summary>
        private void SetRtlAlignGdi(bool rtl) {
            if (_setRtl) {
                if (!rtl)
                    Win32Utils.SetTextAlign(_hdc, Win32Utils.TextAlignDefault);
            } else if (rtl) {
                Win32Utils.SetTextAlign(_hdc, Win32Utils.TextAlignRtl);
            }
            _setRtl = rtl;
        }

        /// <summary>
        /// Special draw logic to draw transparent text using GDI.<br/>
        /// 1. Create in-memory DC<br/>
        /// 2. Copy background to in-memory DC<br/>
        /// 3. Draw the text to in-memory DC<br/>
        /// 4. Copy the in-memory DC to the proper location with alpha blend<br/>
        /// </summary>
        private static void DrawTransparentText(IntPtr hdc, string str, RFont font, Point point, Size size, Color color) {
            IntPtr dib;
            var memoryHdc = Win32Utils.CreateMemoryHdc(hdc, size.Width, size.Height, out dib);

            try {
                // copy target background to memory HDC so when copied back it will have the proper background
                Win32Utils.BitBlt(memoryHdc, 0, 0, size.Width, size.Height, hdc, point.X, point.Y, Win32Utils.BitBltCopy);

                // Create and select font
                Win32Utils.SelectObject(memoryHdc, ((FontAdapter) font).HFont);
                Win32Utils.SetTextColor(memoryHdc, (color.B & 0xFF) << 16 | (color.G & 0xFF) << 8 | color.R);

                // Draw text to memory HDC
                Win32Utils.TextOut(memoryHdc, 0, 0, str, str.Length);

                // copy from memory HDC to normal HDC with alpha blend so achieve the transparent text
                Win32Utils.AlphaBlend(hdc, point.X, point.Y, size.Width, size.Height, memoryHdc, 0, 0, size.Width, size.Height, new BlendFunction(color.A));
            } finally {
                Win32Utils.ReleaseMemoryHdc(memoryHdc, dib);
            }
        }
#endif

        /// <summary>
        /// Change text align to Left-to-Right or Right-to-Left if required.
        /// </summary>
        private void SetRtlAlignGdiPlus(bool rtl) {
            if (_setRtl) {
                if (!rtl)
                    _stringFormat2.FormatFlags ^= StringFormatFlags.DirectionRightToLeft;
            } else if (rtl) {
                _stringFormat2.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
            }
            _setRtl = rtl;
        }

        #endregion
    }
}