using System;
using System.Collections.Generic;
//using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms.Design;

namespace FancyControls
{
    /// <summary>
    /// textbox can show background text and be decorated with controls on left and right side
    /// </summary>
    [ToolboxBitmapAttribute(typeof(TextBox))]
    [DesignerAttribute(typeof(FancyTextBoxDesigner))]
    public partial class FancyTextBox : TextBox
    {
        #region private members
        private TextBoxCustomize customize = null; // inside control painter
        private List<Control> onLeft = null; // inside controls on left
        private List<Control> onRight = null; // inside controls on right
        private string backText = String.Empty; // background text
        private bool backTextShowOnFocus = true; // show background text when active 

        // do repaint when handle created
        private void Repaint()
        {
            // should have customize object when handle created
            if (customize != null)
            {
                // repaint inside controls
                foreach (Control ctl in this.Controls)
                    // remove event handler to avoid infinite loop
                    // as repoaint methos might change location
                    ctl.LocationChanged -= new EventHandler(control_LocationChanged);
                customize.Repaint();
                foreach (Control ctl in this.Controls)
                    // restore event handler
                    ctl.LocationChanged += new EventHandler(control_LocationChanged);
            }
        }

        // add control right/left based on location
        private void AddControl(Control control)
        {
            if (control.Location.X <= Size.Width / 2) // which side to add control
            {
                // add on left
                if (onRight.IndexOf(control) >= 0) 
                {
                    // if it was on right side remove from right side
                    onRight.Remove(control);
                }
                if (onLeft.IndexOf(control) < 0)
                {
                    // if control is not already on left add it
                    onLeft.Add(control);
                }
            }
            else
            {
                // add on right
                if (onLeft.IndexOf(control) >= 0)
                    // if it was on left side remove from left side
                    onLeft.Remove(control);
                if (onRight.IndexOf(control) < 0)
                    // if control is not already on right add it
                    onRight.Add(control);
            }
            Repaint();
        }

        // handler when control is moved from left to right or vice versa
        private void control_LocationChanged(object sender, EventArgs e)
        {
            AddControl((Control)sender);
        }

        // handler to reset edit box margins when control hidden/shown
        private void control_VisibleChanged(object sender, EventArgs e)
        {
            Repaint();
        }
        #endregion

        #region public properties
        /// <summary>
        /// get/set backgorund text 
        /// to show purpose of textbox when textbox is empty
        /// </summary>
        [Category("Appearance")]
        [Description("Background text displays gray text when textbox is empty")]
        [Localizable(true)]
        public string BackText
        {
            get { return (this.backText); }
            set
            {
                if (this.backText != value)
                {
                    this.backText = value;
                    Repaint(); // paint new background text
                }
            }
        }

        /// <summary>
        /// get/set whether to show background text 
        /// when control is in focus
        /// </summary>
        [Category("Appearance")]
        [Description("Background text still displays when in focus")]
        [DefaultValue(true)]
        public bool BackTextShowOnFocus
        {
            get { return (this.backTextShowOnFocus); }
            set
            {
                if (this.backTextShowOnFocus != value)
                {
                    this.backTextShowOnFocus = value;
                    Repaint();
                }
            }
        }

        /// <summary>
        /// is control on left or right side
        /// </summary>
        /// <param name="ctl">control to check</param>
        /// <returns>true - on right/false - on left</returns>
        public bool IsRightSide(Control ctl)
        {
            return (this.onRight.IndexOf(ctl) >= 0);
        }
        #endregion

        #region protected methods overrides
        /// <summary>
        /// set customization object handle
        /// </summary>
        /// <param name="e"></param>
        protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);
            if (customize == null)
                customize = new TextBoxCustomize(this); // create new custom painter object
            else
                customize.SetHandle(this.Handle);
        }

        /// <summary>
        /// release customization object handle
        /// </summary>
        /// <param name="e"></param>
        protected override void OnHandleDestroyed(EventArgs e)
        {
            customize.ReleaseHandle();
            base.OnHandleDestroyed(e);
        }

        /// <summary>
        /// Insert new control on right or left side of textbox
        /// </summary>
        /// <param name="e"></param>
        protected override void OnControlAdded(ControlEventArgs e)
        {
            base.OnControlAdded(e);
            AddControl(e.Control);
            e.Control.LocationChanged += new EventHandler(control_LocationChanged);
            e.Control.VisibleChanged += new EventHandler(control_VisibleChanged);
        }

        /// <summary>
        /// Remove control from textbox
        /// </summary>
        /// <param name="e"></param>
        protected override void OnControlRemoved(ControlEventArgs e)
        {
            base.OnControlRemoved(e);
            onLeft.Remove(e.Control);
            onRight.Remove(e.Control);
            e.Control.LocationChanged -= new EventHandler(control_LocationChanged);
            e.Control.VisibleChanged -= new EventHandler(control_VisibleChanged);
            Repaint();
        }
        #endregion

        #region constructor
        /// <summary>
        /// default constructor
        /// </summary>
        public FancyTextBox()
            : base()
        {
            // create lists to handle right/left controls
            onLeft = new List<Control>(); 
            onRight = new List<Control>();
        }
        #endregion

        #region Designer
        /// <summary>
        /// Render FancyTextBox as container control at design time
        /// to allow dropping of other controls into it
        /// </summary>
        internal class FancyTextBoxDesigner : ParentControlDesigner
        {
            private Control dropControl = null; // dropped control

            /// <summary>
            /// Catches control being dropped into FancyTextBox
            /// </summary>
            /// <param name="control">dropped control</param>
            /// <returns>true/false</returns>
            public override bool CanParent(Control control)
            {
                // NOTE: OnDragDrop event does not give control being dropped
                //       so CanParent fires just before DragDrop where control is
                //       captured and set into dropControl private var.
                dropControl = control;
                return base.CanParent(control);
            }

            /// <summary>
            /// Set dropped control on left/right depending where it was dropped
            /// in FancyTextBox
            /// </summary>
            /// <param name="de">event agrs</param>
            protected override void OnDragDrop(DragEventArgs de)
            {
                try
                {
                    if (dropControl != null)
                    {
                        // set control location to dropped point
                        dropControl.Location = this.Control.PointToClient(new Point(de.X, de.Y));
                        if (this.Control.Controls.IndexOf(dropControl) >= 0)
                        {
                            // if control already belongs to FancyTextBox 
                            // reset side based on new location
                            ((FancyTextBox)this.Control).AddControl(dropControl);
                        }
                        else
                        {
                            // add new c ontrol (handled in OnControlAdded)
                            base.OnDragDrop(de);
                        }
                        dropControl = null;
                    }
                    else
                        base.OnDragDrop(de);
                }
                catch
                {
                    de.Effect = DragDropEffects.None;
                }
            }
        }
        #endregion
    }


        /// <summary>
        /// FancyTextBox custom painter
        /// This Control was inspired by code found in 
        /// http://www.vbaccelerator.com/
        /// </summary>
        internal class TextBoxCustomize : NativeWindow
        {
            #region UnmanagedMethods
            [StructLayout(LayoutKind.Sequential)]
            private struct RECT
            {
                public int left;
                public int top;
                public int right;
                public int bottom;
            }

            [DllImport("user32", CharSet = CharSet.Auto)]
            private extern static int SendMessage(
                IntPtr hwnd,
                int wMsg,
                int wParam,
                int lParam);
            [DllImport("user32", CharSet = CharSet.Auto)]
            private extern static int GetWindowLong(
                IntPtr hWnd,
                int dwStyle);
            [DllImport("user32")]
            private extern static IntPtr GetDC(
                IntPtr hwnd);
            [DllImport("user32")]
            private extern static int ReleaseDC(
                IntPtr hwnd,
                IntPtr hdc);
            [DllImport("user32")]
            private extern static int GetClientRect(
                IntPtr hwnd,
                ref RECT rc);
            [DllImport("user32")]
            private extern static int GetWindowRect(
                IntPtr hwnd,
                ref RECT rc);

            private const int EC_LEFTMARGIN = 0x1;
            private const int EC_RIGHTMARGIN = 0x2;
            private const int EC_USEFONTINFO = 0xFFFF;
            private const int EM_SETMARGINS = 0xD3;
            private const int EM_GETMARGINS = 0xD4;

            private const int WM_PAINT = 0xF;

            private const int WM_SETFOCUS = 0x7;
            private const int WM_KILLFOCUS = 0x8;

            private const int WM_SETFONT = 0x30;

            private const int WM_MOUSEMOVE = 0x200;
            private const int WM_LBUTTONDOWN = 0x201;
            private const int WM_RBUTTONDOWN = 0x204;
            private const int WM_MBUTTONDOWN = 0x207;
            private const int WM_LBUTTONUP = 0x202;
            private const int WM_RBUTTONUP = 0x205;
            private const int WM_MBUTTONUP = 0x208;
            private const int WM_LBUTTONDBLCLK = 0x203;
            private const int WM_RBUTTONDBLCLK = 0x206;
            private const int WM_MBUTTONDBLCLK = 0x209;

            private const int WM_KEYDOWN = 0x0100;
            private const int WM_KEYUP = 0x0101;
            private const int WM_CHAR = 0x0102;


            private const int GWL_EXSTYLE = (-20);
            private const int WS_EX_RIGHT = 0x00001000;
            private const int WS_EX_LEFT = 0x00000000;
            private const int WS_EX_RTLREADING = 0x00002000;
            private const int WS_EX_LTRREADING = 0x00000000;
            private const int WS_EX_LEFTSCROLLBAR = 0x00004000;
            private const int WS_EX_RIGHTSCROLLBAR = 0x00000000;

            #endregion

            #region control to customize and SetHandle
            private FancyTextBox fancyTextBox = null; 
            /// <summary>
            /// Set Native Window handle
            /// </summary>
            /// <param name="Hwnd">Edit Box Handle</param>
            public void SetHandle(IntPtr Hwnd)
            {
                if(!this.Handle.Equals(Hwnd))
                {
                    if (!Hwnd.Equals(IntPtr.Zero))
                    {
                        this.ReleaseHandle();
                    }
                    this.AssignHandle(Hwnd);
                    Repaint();
                }
            }
            #endregion

            #region Unamanaged Methods Wrappers
            // Checks Right to left (not implemented)
            private static bool IsRightToLeft(IntPtr handle)
            {
                int style = GetWindowLong(handle, GWL_EXSTYLE);
                return (
                    ((style & WS_EX_RIGHT) == WS_EX_RIGHT) ||
                    ((style & WS_EX_RTLREADING) == WS_EX_RTLREADING) ||
                    ((style & WS_EX_LEFTSCROLLBAR) == WS_EX_LEFTSCROLLBAR));
            }
            // get edit box right margin
            private static int GetRightMargin(IntPtr handle)
            {
                return (SendMessage(handle, EM_GETMARGINS, 0, 0) / 0x10000);
            }
            // set edit box right box margin
            private static void SetRightMargin(IntPtr handle, int margin)
            {
                SendMessage(handle, EM_SETMARGINS, EC_RIGHTMARGIN, margin * 0x10000);
            }
            // get edit box left margin
            private static int GetLeftMargin(IntPtr handle)
            {
                return (SendMessage(handle, EM_GETMARGINS, 0, 0) & 0xFFFF);
            }
            // set edit box left margin
            private static void SetLeftMargin(IntPtr handle, int margin)
            {
                SendMessage(handle, EM_SETMARGINS, EC_LEFTMARGIN, margin & 0xFFFF);
            }
            // RightMargin property simplifies handling of edit box margin
            private int RightMargin
            {
                get { return (this.Handle != (IntPtr)0 ? GetRightMargin(this.Handle) : 0); }
                set { if (this.Handle != (IntPtr)0) SetRightMargin(this.Handle, value); }
            }
            // LeftMargin property simplifies handling of edit box margin
            private int LeftMargin
            {
                get { return (this.Handle != (IntPtr)0 ? GetLeftMargin(this.Handle) : 0); }
                set { if (this.Handle != (IntPtr)0) SetLeftMargin(this.Handle, value); }
            }
            #endregion

            #region Repaint routines
            /// <summary>
            /// Calls the base WndProc and performs WM_PAINT
            /// processing to repaint inside controls when necessary
            /// </summary>
            /// <param name="m">Windows Message</param>
            protected override void WndProc(ref Message m)
            {
                base.WndProc(ref m);
                switch (m.Msg)
                {
                    case WM_SETFONT:
                    case WM_PAINT:
                    case WM_SETFOCUS:
                    case WM_KILLFOCUS:
                    case WM_LBUTTONDOWN:
                    case WM_RBUTTONDOWN:
                    case WM_MBUTTONDOWN:
                    case WM_LBUTTONUP:
                    case WM_RBUTTONUP:
                    case WM_MBUTTONUP:
                    case WM_LBUTTONDBLCLK:
                    case WM_RBUTTONDBLCLK:
                    case WM_MBUTTONDBLCLK:
                    case WM_KEYDOWN:
                    case WM_CHAR:
                    case WM_KEYUP:
                        Repaint();
                        break;
                    case WM_MOUSEMOVE:
                        if (!m.WParam.Equals(IntPtr.Zero))
                        {
                            Repaint();
                        }
                        break;
                }
            }

            /// <summary>
            /// Paints inside controls and background text when necessary:
            /// </summary>
            public void Repaint()
            {
                // get client rect of edit box
                RECT rcClient = new RECT();
                GetClientRect(this.Handle, ref rcClient);
                RectangleF clientrect = new RectangleF(
                    rcClient.left, rcClient.top,
                    rcClient.right - rcClient.left,
                    rcClient.bottom - rcClient.top);
                // fixed single border is painted inside edit box
                // in this case reduce rect to exclude border
                if (fancyTextBox.BorderStyle == BorderStyle.FixedSingle)
                {
                    clientrect.X += 1;
                    clientrect.Y += 1;
                    clientrect.Width -= 2;
                    clientrect.Height -= 2;
                }
                // check of edit box margins changed due to inside controls changes
                if (MarginChanged(clientrect)) 
                {
                    // margin change triggers repaint event again
                    return;
                }
                // get DC for painting
                IntPtr handle = this.Handle;
                IntPtr hdc = GetDC(handle);
                Graphics gfx = Graphics.FromHdc(hdc);

                int leftMargin = 0;
                int rightMargin = 0;
                // repaint inside controls
                foreach (Control ctl in fancyTextBox.Controls)
                {
                    if (ctl.Visible)
                    {
                        if (fancyTextBox.IsRightSide(ctl))
                        {
                            // repaint control on right 
                            // and calculate next control position
                            rightMargin +=
                                RepaintControl(gfx, ctl, clientrect,
                                    rightMargin, fancyTextBox.IsRightSide(ctl));
                        }
                        else
                        {
                            // repaint control on left 
                            // and calculate next control position
                            leftMargin +=
                                RepaintControl(gfx, ctl, clientrect,
                                    leftMargin, fancyTextBox.IsRightSide(ctl));
                        }
                    }
                }
                // repaint background text if on Text present in FancyTextBox
                if ((this.fancyTextBox.Multiline && this.fancyTextBox.Lines.Length == 0) ||
                    (!this.fancyTextBox.Multiline && this.fancyTextBox.Text == String.Empty))
                {
                    RepaintBackGround(gfx, clientrect);
                }
                // cleanup DC
                gfx.Dispose();
                ReleaseDC(handle, hdc);
            }

            private int RepaintControl(Graphics gfx, Control control,
                RectangleF clientrect, int margin, bool isrightside)
            {
                // scale control to fit exactly inside FancyTextBox height
                SizeF drawSize = ScaledSize(clientrect, control.Size);
                // control's new location
                Point drawLoc = new Point();
                // set control size
                control.Width = (int)drawSize.Width;
                control.Height = (int)drawSize.Height;
                // set control Y loc inside FancyTextBox centered vertically
                drawLoc.Y = (int)(clientrect.Top + (clientrect.Height - drawSize.Height) / 2);
                if (isrightside)
                {
                    // if control is no right side set control X loc
                    drawLoc.X = (int)(clientrect.X + clientrect.Width - margin - drawSize.Width);
                }
                else
                {
                    // if control is no left side set control X loc
                    drawLoc.X = (int)(clientrect.X + margin + drawLoc.Y);
                }
                // move control to new location inside FancyTextBox
                control.Location = drawLoc;
                control.BringToFront();
                return ((int)(drawSize.Width + drawLoc.Y * 2));
            }

            private void RepaintBackGround(Graphics gfx, RectangleF clientrect)
            {
                // calculate background text size
                SizeF textSize = new SizeF();
                if (fancyTextBox.BackText != String.Empty)
                    textSize = gfx.MeasureString(fancyTextBox.BackText, fancyTextBox.Font);
                else
                    // if backgoround text is empty calc largest letter
                    textSize = gfx.MeasureString("W", fancyTextBox.Font);
                // clip rect for background text
                RectangleF cliprect = new RectangleF();
                // start from left margin and top
                cliprect.X = LeftMargin;
                cliprect.Y = clientrect.Y;
                if (fancyTextBox.Multiline)
                {
                    // keep text on top of multiline
                    cliprect.Height = textSize.Height;
                }
                else
                {
                    // center vertically in singleline
                    cliprect.Height = clientrect.Height;
                }
                // set width between margins
                cliprect.Width = clientrect.Width - LeftMargin - RightMargin;
                // background text start location
                PointF textLoc = new PointF();
                // set text start X based on TextAlign value
                switch (fancyTextBox.TextAlign)
                {
                    case HorizontalAlignment.Left:
                        textLoc.X = LeftMargin;
                        break;
                    case HorizontalAlignment.Right:
                        textLoc.X = clientrect.Width - RightMargin - textSize.Width;
                        break;
                    case HorizontalAlignment.Center:
                        textLoc.X = (clientrect.Width - textSize.Width) / 2;
                        break;
                }
                // center Y vertically
                textLoc.Y = cliprect.Top + (cliprect.Height - textSize.Height) / 2;
                // draw text in this rect
                RectangleF stringrect = new RectangleF(textLoc, textSize);
                // adjust string rect for single border
                if (fancyTextBox.BorderStyle == BorderStyle.FixedSingle)
                {
                    cliprect.X += 1;
                    cliprect.Y += 1;
                    cliprect.Width -= 2;
                    cliprect.Height -= 2;
                }
                gfx.SetClip(cliprect);
                // erase old background text
                // (this sometimes wipes out design move container handle partially)
                EraseBackground(gfx);
                // draw background text when appropriate
                if (fancyTextBox.BackTextShowOnFocus || !fancyTextBox.Focused)
                {
                    Brush stringbrush = new SolidBrush(SystemColors.GrayText);
                    gfx.DrawString(fancyTextBox.BackText, fancyTextBox.Font, stringbrush, stringrect);
                    stringbrush.Dispose();
                }
                gfx.ResetClip();
            }

            private void EraseBackground(Graphics gfx)
            {
                // Fill with background color
                SolidBrush brush = new SolidBrush(fancyTextBox.BackColor);
                gfx.FillRegion(brush, gfx.Clip);
                brush.Dispose();
            }
            // calculate new margins
            private bool MarginChanged(RectangleF clientrect)
            {
                int rightMargin = 0;
                int leftMargin = 0;
                foreach (Control ctl in fancyTextBox.Controls)
                {
                    if (ctl.Visible)
                    {
                        SizeF scaledSize = ScaledSize(clientrect,
                            new SizeF(ctl.Size));
                        if (fancyTextBox.IsRightSide(ctl))
                        {
                            rightMargin += (int)scaledSize.Width;
                            if (scaledSize.Height < clientrect.Height)
                                rightMargin += (int)(clientrect.Height - scaledSize.Height);
                        }
                        else
                        {
                            leftMargin += (int)scaledSize.Width;
                            if (scaledSize.Height < clientrect.Height)
                                leftMargin += (int)(clientrect.Height - scaledSize.Height);
                        }
                    }
                }
                bool result = false;
                if (LeftMargin != leftMargin)
                {
                    result = true;
                    LeftMargin = leftMargin;
                }
                if (RightMargin != rightMargin)
                {
                    result = true;
                    RightMargin = rightMargin;
                }
                return (result);
            }
            // scale control to fit FancyTextBox
            private SizeF ScaledSize(RectangleF clientrect, SizeF size)
            {
                SizeF scaledSize = new SizeF(size);
                scaledSize.Width *= clientrect.Height / scaledSize.Height;
                scaledSize.Height = clientrect.Height;
                return (scaledSize);
            }
            #endregion

            #region Constructors
            /// <summary>
            /// Constructs a new instance of this class
            /// </summary>
            public TextBoxCustomize() : base()
            {
            }

            /// <summary>
            /// Constructs instance with FancyTextBox attached
            /// </summary>
            /// <param name="ftb">FancyTextBox</param>
            public TextBoxCustomize(FancyTextBox ftb) : base()
            {
                this.fancyTextBox = ftb;
                this.SetHandle(ftb.Handle);
            }
            #endregion
        }
    }