using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;

namespace OpenNest
{
    public enum ToolbarTheme
    {
        Toolbar,
        MediaToolbar,
        CommunicationsToolbar,
        BrowserTabBar,
        HelpBar
    }

    /// <summary>Renders a toolstrip using the UxTheme API via VisualStyleRenderer and a specific style.</summary>
    /// <remarks>Perhaps surprisingly, this does not need to be disposable.</remarks>
    public class ToolStripRenderer : ToolStripSystemRenderer
    {
        VisualStyleRenderer renderer;

        public ToolStripRenderer(ToolbarTheme theme)
        {
            Theme = theme;
        }

        /// <summary>
        /// It shouldn't be necessary to P/Invoke like this, however VisualStyleRenderer.GetMargins
        /// misses out a parameter in its own P/Invoke.
        /// </summary>
        static internal class NativeMethods
        {
            [StructLayout(LayoutKind.Sequential)]
            public struct MARGINS
            {
                public int cxLeftWidth;
                public int cxRightWidth;
                public int cyTopHeight;
                public int cyBottomHeight;
            }

            [DllImport("uxtheme.dll")]
            public extern static int GetThemeMargins(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, int iPropId, IntPtr rect, out MARGINS pMargins);
        }

        // See http://msdn2.microsoft.com/en-us/library/bb773210.aspx - "Parts and States"
        // Only menu-related parts/states are needed here, VisualStyleRenderer handles most of the rest.
        enum MenuParts : int
        {
            ItemTMSchema = 1,
            DropDownTMSchema = 2,
            BarItemTMSchema = 3,
            BarDropDownTMSchema = 4,
            ChevronTMSchema = 5,
            SeparatorTMSchema = 6,
            BarBackground = 7,
            BarItem = 8,
            PopupBackground = 9,
            PopupBorders = 10,
            PopupCheck = 11,
            PopupCheckBackground = 12,
            PopupGutter = 13,
            PopupItem = 14,
            PopupSeparator = 15,
            PopupSubmenu = 16,
            SystemClose = 17,
            SystemMaximize = 18,
            SystemMinimize = 19,
            SystemRestore = 20
        }

        enum MenuBarStates : int
        {
            Active = 1,
            Inactive = 2
        }

        enum MenuBarItemStates : int
        {
            Normal = 1,
            Hover = 2,
            Pushed = 3,
            Disabled = 4,
            DisabledHover = 5,
            DisabledPushed = 6
        }

        enum MenuPopupItemStates : int
        {
            Normal = 1,
            Hover = 2,
            Disabled = 3,
            DisabledHover = 4
        }

        enum MenuPopupCheckStates : int
        {
            CheckmarkNormal = 1,
            CheckmarkDisabled = 2,
            BulletNormal = 3,
            BulletDisabled = 4
        }

        enum MenuPopupCheckBackgroundStates : int
        {
            Disabled = 1,
            Normal = 2,
            Bitmap = 3
        }

        enum MenuPopupSubMenuStates : int
        {
            Normal = 1,
            Disabled = 2
        }

        enum MarginTypes : int
        {
            Sizing = 3601,
            Content = 3602,
            Caption = 3603
        }

        static readonly int RebarBackground = 6;

        Padding GetThemeMargins(IDeviceContext dc, MarginTypes marginType)
        {
            NativeMethods.MARGINS margins;
            try
            {
                IntPtr hDC = dc.GetHdc();
                if (0 == NativeMethods.GetThemeMargins(renderer.Handle, hDC, renderer.Part, renderer.State, (int)marginType, IntPtr.Zero, out margins))
                    return new Padding(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight);
                return new Padding(0);
            }
            finally
            {
                dc.ReleaseHdc();
            }
        }

        private static int GetItemState(ToolStripItem item)
        {
            bool hot = item.Selected;

            if (item.IsOnDropDown)
            {
                if (item.Enabled)
                    return hot ? (int)MenuPopupItemStates.Hover : (int)MenuPopupItemStates.Normal;
                return hot ? (int)MenuPopupItemStates.DisabledHover : (int)MenuPopupItemStates.Disabled;
            }
            else
            {
                if (item.Pressed)
                    return item.Enabled ? (int)MenuBarItemStates.Pushed : (int)MenuBarItemStates.DisabledPushed;
                if (item.Enabled)
                    return hot ? (int)MenuBarItemStates.Hover : (int)MenuBarItemStates.Normal;
                return hot ? (int)MenuBarItemStates.DisabledHover : (int)MenuBarItemStates.Disabled;
            }
        }

        public ToolbarTheme Theme
        {
            get;
            set;
        }

        private string RebarClass
        {
            get
            {
                return SubclassPrefix + "Rebar";
            }
        }

        private string ToolbarClass
        {
            get
            {
                return SubclassPrefix + "ToolBar";
            }
        }

        private string MenuClass
        {
            get
            {
                return SubclassPrefix + "Menu";
            }
        }

        private string SubclassPrefix
        {
            get
            {
                switch (Theme)
                {
                    case ToolbarTheme.MediaToolbar: return "Media::";
                    case ToolbarTheme.CommunicationsToolbar: return "Communications::";
                    case ToolbarTheme.BrowserTabBar: return "BrowserTabBar::";
                    case ToolbarTheme.HelpBar: return "Help::";
                    default: return string.Empty;
                }
            }
        }

        private VisualStyleElement Subclass(VisualStyleElement element)
        {
            return VisualStyleElement.CreateElement(SubclassPrefix + element.ClassName,
                    element.Part, element.State);
        }

        private bool EnsureRenderer()
        {
            if (!IsSupported)
                return false;

            if (renderer == null)
                renderer = new VisualStyleRenderer(VisualStyleElement.Button.PushButton.Normal);

            return true;
        }

        // Gives parented ToolStrips a transparent background.
        protected override void Initialize(ToolStrip toolStrip)
        {
            if (toolStrip.Parent is ToolStripPanel)
                toolStrip.BackColor = Color.Transparent;

            base.Initialize(toolStrip);
        }

        // Using just ToolStripManager.Renderer without setting the Renderer individually per ToolStrip means
        // that the ToolStrip is not passed to the Initialize method. ToolStripPanels, however, are. So we can 
        // simply initialize it here too, and this should guarantee that the ToolStrip is initialized at least 
        // once. Hopefully it isn't any more complicated than this.
        protected override void InitializePanel(ToolStripPanel toolStripPanel)
        {
            foreach (Control control in toolStripPanel.Controls)
                if (control is ToolStrip)
                    Initialize((ToolStrip)control);

            base.InitializePanel(toolStripPanel);
        }

        protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                renderer.SetParameters(MenuClass, (int)MenuParts.PopupBorders, 0);
                if (e.ToolStrip.IsDropDown)
                {
                    Region oldClip = e.Graphics.Clip;

                    // Tool strip borders are rendered *after* the content, for some reason.
                    // So we have to exclude the inside of the popup otherwise we'll draw over it.
                    Rectangle insideRect = e.ToolStrip.ClientRectangle;
                    insideRect.Inflate(-1, -1);
                    e.Graphics.ExcludeClip(insideRect);

                    renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds);

                    // Restore the old clip in case the Graphics is used again (does that ever happen?)
                    e.Graphics.Clip = oldClip;
                }
            }
            else
            {
                base.OnRenderToolStripBorder(e);
            }
        }

        Rectangle GetBackgroundRectangle(ToolStripItem item)
        {
            if (!item.IsOnDropDown)
                return new Rectangle(new Point(), item.Bounds.Size);

            // For a drop-down menu item, the background rectangles of the items should be touching vertically.
            // This ensures that's the case.
            Rectangle rect = item.Bounds;

            // The background rectangle should be inset two pixels horizontally (on both sides), but we have 
            // to take into account the border.
            rect.X = item.ContentRectangle.X + 1;
            rect.Width = item.ContentRectangle.Width - 1;

            // Make sure we're using all of the vertical space, so that the edges touch.
            rect.Y = 0;
            return rect;
        }

        protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                int partID = e.Item.IsOnDropDown ? (int)MenuParts.PopupItem : (int)MenuParts.BarItem;
                renderer.SetParameters(MenuClass, partID, GetItemState(e.Item));

                Rectangle bgRect = GetBackgroundRectangle(e.Item);
                renderer.DrawBackground(e.Graphics, bgRect, bgRect);
            }
            else
            {
                base.OnRenderMenuItemBackground(e);
            }
        }

        protected override void OnRenderToolStripPanelBackground(ToolStripPanelRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                // Draw the background using Rebar & RP_BACKGROUND (or, if that is not available, fall back to
                // Rebar.Band.Normal)
                if (VisualStyleRenderer.IsElementDefined(VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0)))
                {
                    renderer.SetParameters(RebarClass, RebarBackground, 0);
                }
                else
                {
                    renderer.SetParameters(RebarClass, 0, 0);
                }

                if (renderer.IsBackgroundPartiallyTransparent())
                    renderer.DrawParentBackground(e.Graphics, e.ToolStripPanel.ClientRectangle, e.ToolStripPanel);

                renderer.DrawBackground(e.Graphics, e.ToolStripPanel.ClientRectangle);

                e.Handled = true;
            }
            else
            {
                base.OnRenderToolStripPanelBackground(e);
            }
        }

        // Render the background of an actual menu bar, dropdown menu or toolbar.
        protected override void OnRenderToolStripBackground(System.Windows.Forms.ToolStripRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                if (e.ToolStrip.IsDropDown)
                {
                    renderer.SetParameters(MenuClass, (int)MenuParts.PopupBackground, 0);
                }
                else
                {
                    // It's a MenuStrip or a ToolStrip. If it's contained inside a larger panel, it should have a
                    // transparent background, showing the panel's background.

                    if (e.ToolStrip.Parent is ToolStripPanel)
                    {
                        // The background should be transparent, because the ToolStripPanel's background will be visible.
                        // (Of course, we assume the ToolStripPanel is drawn using the same theme, but it's not my fault
                        // if someone does that.)
                        return;
                    }
                    else
                    {
                        // A lone toolbar/menubar should act like it's inside a toolbox, I guess.
                        // Maybe I should use the MenuClass in the case of a MenuStrip, although that would break
                        // the other themes...
                        if (VisualStyleRenderer.IsElementDefined(VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0)))
                            renderer.SetParameters(RebarClass, RebarBackground, 0);
                        else
                            renderer.SetParameters(RebarClass, 0, 0);
                    }
                }

                if (renderer.IsBackgroundPartiallyTransparent())
                    renderer.DrawParentBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.ToolStrip);

                renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds);
            }
            else
            {
                base.OnRenderToolStripBackground(e);
            }
        }

        // The only purpose of this override is to change the arrow colour.
        // It's OK to just draw over the default arrow since we also pass down arrow drawing to the system renderer.
        protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                ToolStripSplitButton sb = (ToolStripSplitButton)e.Item;
                base.OnRenderSplitButtonBackground(e);

                // It doesn't matter what colour of arrow we tell it to draw. OnRenderArrow will compute it from the item anyway.
                OnRenderArrow(new ToolStripArrowRenderEventArgs(e.Graphics, sb, sb.DropDownButtonBounds, Color.Red, ArrowDirection.Down));
            }
            else
            {
                base.OnRenderSplitButtonBackground(e);
            }
        }

        Color GetItemTextColor(ToolStripItem item)
        {
            int partId = item.IsOnDropDown ? (int)MenuParts.PopupItem : (int)MenuParts.BarItem;
            renderer.SetParameters(MenuClass, partId, GetItemState(item));
            return renderer.GetColor(ColorProperty.TextColor);
        }

        protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
        {
            if (EnsureRenderer())
                e.TextColor = GetItemTextColor(e.Item);

            base.OnRenderItemText(e);
        }

        protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                if (e.ToolStrip.IsDropDown)
                {
                    renderer.SetParameters(MenuClass, (int)MenuParts.PopupGutter, 0);
                    // The AffectedBounds is usually too small, way too small to look right. Instead of using that,
                    // use the AffectedBounds but with the right width. Then narrow the rectangle to the correct edge
                    // based on whether or not it's RTL. (It doesn't need to be narrowed to an edge in LTR mode, but let's
                    // do that anyway.)
                    // Using the DisplayRectangle gets roughly the right size so that the separator is closer to the text.
                    Padding margins = GetThemeMargins(e.Graphics, MarginTypes.Sizing);
                    int extraWidth = (e.ToolStrip.Width - e.ToolStrip.DisplayRectangle.Width - margins.Left - margins.Right - 1) - e.AffectedBounds.Width;
                    Rectangle rect = e.AffectedBounds;
                    rect.Y += 2;
                    rect.Height -= 4;
                    int sepWidth = renderer.GetPartSize(e.Graphics, ThemeSizeType.True).Width;
                    if (e.ToolStrip.RightToLeft == RightToLeft.Yes)
                    {
                        rect = new Rectangle(rect.X - extraWidth, rect.Y, sepWidth, rect.Height);
                        rect.X += sepWidth;
                    }
                    else
                    {
                        rect = new Rectangle(rect.Width + extraWidth - sepWidth, rect.Y, sepWidth, rect.Height);
                    }
                    renderer.DrawBackground(e.Graphics, rect);
                }
            }
            else
            {
                base.OnRenderImageMargin(e);
            }
        }

        protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e)
        {
            if (e.ToolStrip.IsDropDown && EnsureRenderer())
            {
                renderer.SetParameters(MenuClass, (int)MenuParts.PopupSeparator, 0);
                Rectangle rect = new Rectangle(e.ToolStrip.DisplayRectangle.Left, 0, e.ToolStrip.DisplayRectangle.Width, e.Item.Height);
                renderer.DrawBackground(e.Graphics, rect, rect);
            }
            else
            {
                e.Graphics.DrawLine(Pens.LightGray,
                    e.Item.ContentRectangle.X,
                    e.Item.ContentRectangle.Y,
                    e.Item.ContentRectangle.X,
                    e.Item.ContentRectangle.Y + e.Item.Height - 6);
            }
        }

        protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                Rectangle bgRect = GetBackgroundRectangle(e.Item);
                bgRect.Width = bgRect.Height;

                // Now, mirror its position if the menu item is RTL.
                if (e.Item.RightToLeft == RightToLeft.Yes)
                    bgRect = new Rectangle(e.ToolStrip.ClientSize.Width - bgRect.X - bgRect.Width, bgRect.Y, bgRect.Width, bgRect.Height);

                renderer.SetParameters(MenuClass, (int)MenuParts.PopupCheckBackground, e.Item.Enabled ? (int)MenuPopupCheckBackgroundStates.Normal : (int)MenuPopupCheckBackgroundStates.Disabled);
                renderer.DrawBackground(e.Graphics, bgRect);

                Rectangle checkRect = e.ImageRectangle;
                checkRect.X = bgRect.X + bgRect.Width / 2 - checkRect.Width / 2;
                checkRect.Y = bgRect.Y + bgRect.Height / 2 - checkRect.Height / 2;

                // I don't think ToolStrip even supports radio box items, so no need to render them.
                renderer.SetParameters(MenuClass, (int)MenuParts.PopupCheck, e.Item.Enabled ? (int)MenuPopupCheckStates.CheckmarkNormal : (int)MenuPopupCheckStates.CheckmarkDisabled);

                renderer.DrawBackground(e.Graphics, checkRect);
            }
            else
            {
                base.OnRenderItemCheck(e);
            }
        }

        protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e)
        {
            // The default renderer will draw an arrow for us (the UXTheme API seems not to have one for all directions),
            // but it will get the colour wrong in many cases. The text colour is probably the best colour to use.
            if (EnsureRenderer())
                e.ArrowColor = GetItemTextColor(e.Item);
            base.OnRenderArrow(e);
        }

        protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEventArgs e)
        {
            if (EnsureRenderer())
            {
                // BrowserTabBar::Rebar draws the chevron using the default background. Odd.
                string rebarClass = RebarClass;
                if (Theme == ToolbarTheme.BrowserTabBar)
                    rebarClass = "Rebar";

                int state = VisualStyleElement.Rebar.Chevron.Normal.State;
                if (e.Item.Pressed)
                    state = VisualStyleElement.Rebar.Chevron.Pressed.State;
                else if (e.Item.Selected)
                    state = VisualStyleElement.Rebar.Chevron.Hot.State;

                renderer.SetParameters(rebarClass, VisualStyleElement.Rebar.Chevron.Normal.Part, state);
                renderer.DrawBackground(e.Graphics, new Rectangle(Point.Empty, e.Item.Size));
            }
            else
            {
                base.OnRenderOverflowButtonBackground(e);
            }
        }

        public bool IsSupported
        {
            get
            {
                if (!VisualStyleRenderer.IsSupported)
                    return false;

                // Needs a more robust check. It seems mono supports very different style sets.
                return
                        VisualStyleRenderer.IsElementDefined(
                                VisualStyleElement.CreateElement("Menu",
                                        (int)MenuParts.BarBackground,
                                        (int)MenuBarStates.Active));
            }
        }
    }
}
