using System;
using System.Collections.Generic;
using System.Text;
using ShellDll;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Drawing;

namespace FileBrowser
{
    /// <summary>
    /// This class takes care of every drop operation in a BrowserTreeView
    /// </summary>
    internal class BrowserTVDropWrapper : ShellDll.IDropTarget, IDisposable
    {
        #region Fields

        // The browser for which to do the drop work
        private Browser br;

        private IntPtr treeViewHandle;

        // The current IDropTarget the cursor is over and the pointers to the target and dataobject
        private ShellDll.IDropTarget dropTarget;
        private IntPtr dropTargetPtr;
        private IntPtr dropDataObject;

        private IDropTargetHelper dropHelper;
        private IntPtr dropHelperPtr;

        // The current node the cursor is over to drop on
        private TreeNode dropNode;

        // The last selected node from the browser
        // This is to remember which node to select after drop has completed
        private TreeNode lastSelectedNode;

        // The parent ShellItems of the drop- and dragitem
        private ShellItem parentDropItem, parentDragItem;

        // The mouse and keys state when a drag enter occurs
        private ShellAPI.MK mouseButtons;

        // A bool to indicate whether this class has been disposed
        private bool disposed = false;

        // The event for when a drop is occuring
        public event DropEventHandler Drop;

        #endregion

        /// <summary>
        /// Registers the TreeView for drag/drop operations and uses this class as the IDropTarget
        /// </summary>
        /// <param name="br">The browser for which to support the drop</param>
        public BrowserTVDropWrapper(Browser br)
        {
            this.br = br;

            treeViewHandle = br.FolderView.Handle;
            ShellAPI.RegisterDragDrop(treeViewHandle, this);

            br.FolderView.HandleCreated += new EventHandler(FolderView_HandleCreated);
            br.FolderView.HandleDestroyed += new EventHandler(FolderView_HandleDestroyed);

            ShellHelper.GetIDropTargetHelper(out dropHelperPtr, out dropHelper);
        }

        ~BrowserTVDropWrapper()
        {
            ((IDisposable)this).Dispose();
        }

        #region Handle Changes

        void FolderView_HandleCreated(object sender, EventArgs e)
        {
            treeViewHandle = br.FolderView.Handle;
            ShellAPI.RegisterDragDrop(treeViewHandle, this);
        }

        void FolderView_HandleDestroyed(object sender, EventArgs e)
        {
            ShellAPI.RevokeDragDrop(br.FolderView.Handle);
        }

        #endregion

        #region Public

        /// <summary>
        /// This ShellItem is the parent item of the item being currently dragged. This field is used
        /// to check whether an item is being moved to it's original folder. If this is the case, we don't
        /// have to do anything, cause the item is allready there.
        /// </summary>
        public ShellItem ParentDragItem
        {
            get { return parentDragItem; }
            set { parentDragItem = value; }
        }

        #endregion

        #region Generated Events

        /// <summary>
        /// This event will be raised whenever a drop occurs on the TreeView.
        /// </summary>
        /// <param name="e">The DropEventArgs for the event</param>
        private void OnDrop(DropEventArgs e)
        {
            if (Drop != null)
                Drop(this, e);
        }

        #endregion

        #region IDropTarget Members

        public int DragEnter(IntPtr pDataObj, ShellAPI.MK grfKeyState, ShellAPI.POINT pt, ref DragDropEffects pdwEffect)
        {
            mouseButtons = grfKeyState;

            br.FolderView.Focus();
            br.SelectionChange = false;
            lastSelectedNode = br.FolderView.SelectedNode;

            ReleaseCom();

            dropDataObject = pDataObj;

            #region Get DropItem

            Point point = br.FolderView.PointToClient(new Point(pt.x, pt.y));
            TreeViewHitTestInfo hitTest = br.FolderView.HitTest(point);

            dropNode = hitTest.Node;
            br.FolderView.SelectedNode = dropNode;

            if (dropNode != null)
            {
                ShellItem item = (ShellItem)dropNode.Tag;
                parentDropItem = item;

                if (ShellHelper.GetIDropTarget(item, out dropTargetPtr, out dropTarget))
                {
                    dropTarget.DragEnter(pDataObj, grfKeyState, pt, ref pdwEffect);
                }
            }

            #endregion

            if (dropHelper != null)
                dropHelper.DragEnter(br.Handle, pDataObj, ref pt, pdwEffect);

            return ShellAPI.S_OK;
        }

        public int DragOver(ShellAPI.MK grfKeyState, ShellAPI.POINT pt, ref DragDropEffects pdwEffect)
        {
            bool reset = false;

            #region Get DropItem

            Point point = br.FolderView.PointToClient(new Point(pt.x, pt.y));
            TreeViewHitTestInfo hitTest = br.FolderView.HitTest(point);
            if (!TreeNode.Equals(dropNode, hitTest.Node))
            {
                if (dropTarget != null)
                    dropTarget.DragLeave();

                ReleaseCom();

                dropNode = hitTest.Node;
                br.FolderView.SelectedNode = dropNode;
                
                if (dropNode == null)
                {
                    pdwEffect = DragDropEffects.None;

                    if (dropHelper != null)
                        dropHelper.DragOver(ref pt, pdwEffect);

                    return ShellAPI.S_OK;
                }
                else
                {
                    ShellItem item = (ShellItem)dropNode.Tag;
                    parentDropItem = item;

                    ShellHelper.GetIDropTarget(item, out dropTargetPtr, out dropTarget);
                    reset = true;
                }
            }
            else if (dropNode == null)
            {
                if (dropTarget != null)
                    dropTarget.DragLeave();

                ReleaseCom();

                dropNode = null;
                br.SelectedNode = null;

                pdwEffect = DragDropEffects.None;

                if (dropHelper != null)
                    dropHelper.DragOver(ref pt, pdwEffect);

                return ShellAPI.S_OK;
            }

            #endregion

            if (dropTarget != null)
            {
                if (reset)
                    dropTarget.DragEnter(dropDataObject, grfKeyState, pt, ref pdwEffect);
                else
                    dropTarget.DragOver(grfKeyState, pt, ref pdwEffect);
            }
            else
                pdwEffect = DragDropEffects.None;

            if (dropHelper != null)
                dropHelper.DragOver(ref pt, pdwEffect);

            return ShellAPI.S_OK;
        }

        public int DragLeave()
        {
            ResetDrop();
            if (dropTarget != null)
            {
                dropTarget.DragLeave();

                ReleaseCom();
                dropDataObject = IntPtr.Zero;
            }

            if (dropHelper != null)
                dropHelper.DragLeave();

            return ShellAPI.S_OK;
        }

        public int DragDrop(IntPtr pDataObj, ShellAPI.MK grfKeyState, ShellAPI.POINT pt, ref DragDropEffects pdwEffect)
        {
            OnDrop(new DropEventArgs(mouseButtons, br.FolderView));

            if (!((mouseButtons & ShellAPI.MK.RBUTTON) != 0 ||
                  grfKeyState == ShellAPI.MK.CONTROL || 
                  grfKeyState == ShellAPI.MK.ALT || 
                  grfKeyState == (ShellAPI.MK.CONTROL | ShellAPI.MK.SHIFT)) && ShellItem.Equals(parentDragItem, parentDropItem))
            {
                ResetDrop();                
                ReleaseCom();
                pdwEffect = DragDropEffects.None;

                if (dropHelper != null)
                    dropHelper.DragLeave();

                return ShellAPI.S_OK;
            }

            ResetDrop();
            if (dropTarget != null)
            {
                dropTarget.DragDrop(pDataObj, grfKeyState, pt, ref pdwEffect);

                ReleaseCom();
                dropDataObject = IntPtr.Zero;
            }

            if (dropHelper != null)
                dropHelper.Drop(pDataObj, ref pt, pdwEffect);

            return ShellAPI.S_OK;
        }

        /// <summary>
        /// Reset all fields to the default values and release the IDropTarget
        /// </summary>
        private void ResetDrop()
        {
            if (dropNode != null)
            {
                dropNode = null;
                parentDropItem = null;
            }

            if (lastSelectedNode != null)
                br.FolderView.SelectedNode = lastSelectedNode;

            br.SelectionChange = true;
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// If not disposed, dispose the class
        /// </summary>
        public void Dispose()
        {
            if (!disposed)
            {
                DisposeDropWrapper();
                GC.SuppressFinalize(this);

                disposed = true;
            }
        }

        /// <summary>
        /// Revokes the TreeView from getting drop messages and releases the IDropTarget
        /// </summary>
        private void DisposeDropWrapper()
        {
            ReleaseCom();

            if (dropHelper != null)
            {
                Marshal.ReleaseComObject(dropHelper);
            }
        }

        /// <summary>
        /// Release the IDropTarget and free's the allocated memory
        /// </summary>
        private void ReleaseCom()
        {
            if (dropTarget != null)
            {
                Marshal.ReleaseComObject(dropTarget);

                dropTarget = null;
                dropHelperPtr = IntPtr.Zero;
            }
        }

        #endregion
    }

    /// <summary>
    /// This class takes care of every drop operation in a BrowserListView
    /// </summary>
    internal class BrowserLVDropWrapper : ShellDll.IDropTarget, IDisposable
    {
        #region Fields

        // The browser for which to do the drop work
        private Browser br;
        
        private IntPtr listViewHandle;

        // The current IDropTarget the cursor is over and the pointers to the target and dataobject
        private ShellDll.IDropTarget dropTarget;
        private IntPtr dropTargetPtr;
        private IntPtr dropDataObject;

        private IDropTargetHelper dropHelper;
        private IntPtr dropHelperPtr;

        // The current ListViewItem the cursor is over to drop on
        private ListViewItem dropListItem;

        // The selected state from the dropListItem before the cursor moved over it
        private bool wasSelected;

        // The parent ShellItems of the drop- and dragitem
        private ShellItem parentDropItem, parentDragItem;

        // The mouse and keys state and DragDropEffects when a drag enter occurs
        private ShellAPI.MK mouseButtons;
        private DragDropEffects startEffects;

        // A bool to indicate whether this class has been disposed
        private bool disposed = false;

        // The event for when a drop is occuring
        public event DropEventHandler Drop;

        #endregion

        /// <summary>
        /// Registers the ListView for drag/drop operations and uses this class as the IDropTarget
        /// </summary>
        /// <param name="br">The browser for which to support the drop</param>
        public BrowserLVDropWrapper(Browser br)
        {
            this.br = br;

            listViewHandle = br.FileView.Handle;
            ShellAPI.RegisterDragDrop(listViewHandle, this);

            br.FileView.HandleCreated += new EventHandler(FileView_HandleCreated);
            br.FileView.HandleDestroyed += new EventHandler(FileView_HandleDestroyed);

            ShellHelper.GetIDropTargetHelper(out dropHelperPtr, out dropHelper);
        }

        ~BrowserLVDropWrapper()
        {
            ((IDisposable)this).Dispose();
        }

        #region Handle Changes

        void FileView_HandleCreated(object sender, EventArgs e)
        {
            listViewHandle = br.FileView.Handle;
            ShellAPI.RegisterDragDrop(listViewHandle, this);
        }

        void FileView_HandleDestroyed(object sender, EventArgs e)
        {
            ShellAPI.RevokeDragDrop(listViewHandle);
        }

        #endregion

        #region Public

        /// <summary>
        /// This ShellItem is the parent item of the item being currently dragged. This field is used
        /// to check whether an item is being moved to it's original folder. If this is the case, we don't
        /// have to do anything, cause the item is allready there.
        /// </summary>
        public ShellItem ParentDragItem
        {
            get { return parentDragItem; }
            set { parentDragItem = value; }
        }

        #endregion

        #region Generated Events

        /// <summary>
        /// This event will be raised whenever a drop occurs on the ListView.
        /// </summary>
        /// <param name="e">The DropEventArgs for the event</param>
        private void OnDrop(DropEventArgs e)
        {
            if (Drop != null)
                Drop(this, e);
        }

        #endregion

        #region IDropTarget Members

        public int DragEnter(IntPtr pDataObj, ShellAPI.MK grfKeyState, ShellAPI.POINT pt, ref DragDropEffects pdwEffect)
        {
            mouseButtons = grfKeyState;
            startEffects = pdwEffect;

            br.FileView.Focus();
            br.SelectionChange = false;
            ReleaseCom();

            dropDataObject = pDataObj;

            #region Get DropItem
            Point point = br.FileView.PointToClient(new Point(pt.x, pt.y));
            ListViewHitTestInfo hitTest = br.FileView.HitTest(point);
            if (hitTest.Item != null && 
                (br.FileView.View != View.Details || hitTest.SubItem == null || hitTest.Item.Name == hitTest.SubItem.Name) &&
                (hitTest.Location == ListViewHitTestLocations.Image ||
                 hitTest.Location == ListViewHitTestLocations.Label ||
                 hitTest.Location == ListViewHitTestLocations.StateImage))
            {
                dropListItem = hitTest.Item;

                wasSelected = dropListItem.Selected;
                dropListItem.Selected = true;

                ShellItem item = (ShellItem)dropListItem.Tag;
                parentDropItem = item;

                ShellHelper.GetIDropTarget(item, out dropTargetPtr, out dropTarget);
            }
            else
            {
                dropListItem = null;
                parentDropItem = br.SelectedItem;
                ShellHelper.GetIDropTarget(br.SelectedItem, out dropTargetPtr, out dropTarget);
            }
            #endregion

            if (dropTarget != null)
                dropTarget.DragEnter(pDataObj, grfKeyState, pt, ref pdwEffect);

            if (dropHelper != null)
                dropHelper.DragEnter(br.Handle, pDataObj, ref pt, pdwEffect);

            return ShellAPI.S_OK;
        }

        public int DragOver(ShellAPI.MK grfKeyState, ShellAPI.POINT pt, ref DragDropEffects pdwEffect)
        {
            bool reset = false;

            #region Get DropItem

            Point point = br.FileView.PointToClient(new Point(pt.x, pt.y));
            ListViewHitTestInfo hitTest = br.FileView.HitTest(point);
            if (hitTest.Item != null &&
                (br.FileView.View != View.Details || hitTest.SubItem == null || hitTest.Item.Name == hitTest.SubItem.Name) &&
                (hitTest.Location == ListViewHitTestLocations.Image ||
                 hitTest.Location == ListViewHitTestLocations.Label ||
                 hitTest.Location == ListViewHitTestLocations.StateImage))
            {                
                if (!hitTest.Item.Equals(dropListItem))
                {
                    if (dropTarget != null)
                        dropTarget.DragLeave();

                    ReleaseCom();

                    if (dropListItem != null)
                        dropListItem.Selected = wasSelected;

                    dropListItem = hitTest.Item;
                    wasSelected = dropListItem.Selected;
                    dropListItem.Selected = true;

                    ShellItem item = (ShellItem)dropListItem.Tag;
                    parentDropItem = item;

                    ShellHelper.GetIDropTarget(item, out dropTargetPtr, out dropTarget);
                    reset = true;
                }
            }
            else
            {
                if (dropListItem != null)
                {
                    if (dropTarget != null)
                        dropTarget.DragLeave();

                    ReleaseCom();

                    dropListItem.Selected = wasSelected;

                    dropListItem = null;
                    parentDropItem = br.SelectedItem;

                    ShellHelper.GetIDropTarget(br.SelectedItem, out dropTargetPtr, out dropTarget);
                    reset = true;
                }
            }

            #endregion

            if (dropTarget != null)
            {
                if (reset)
                    dropTarget.DragEnter(dropDataObject, grfKeyState, pt, ref pdwEffect);
                else
                    dropTarget.DragOver(grfKeyState, pt, ref pdwEffect);
            }
            else
                pdwEffect = DragDropEffects.None;

            if (dropHelper != null)
                dropHelper.DragOver(ref pt, pdwEffect);

            return ShellAPI.S_OK;
        }

        public int DragLeave()
        {
            ResetDrop();
            if (dropTarget != null)
            {
                dropTarget.DragLeave();

                ReleaseCom();
                dropDataObject = IntPtr.Zero;
            }

            if (dropHelper != null)
                dropHelper.DragLeave();

            return ShellAPI.S_OK;
        }

        public int DragDrop(IntPtr pDataObj, ShellAPI.MK grfKeyState, ShellAPI.POINT pt, ref DragDropEffects pdwEffect)
        {
            OnDrop(new DropEventArgs(mouseButtons, br.FileView));

            if (!((mouseButtons & ShellAPI.MK.RBUTTON) != 0 ||
                  grfKeyState == ShellAPI.MK.CONTROL ||
                  grfKeyState == ShellAPI.MK.ALT ||
                  grfKeyState == (ShellAPI.MK.CONTROL | ShellAPI.MK.SHIFT)) && ShellItem.Equals(parentDragItem, parentDropItem))
            {
                ResetDrop();
                ReleaseCom();
                pdwEffect = DragDropEffects.None;

                if (dropHelper != null)
                    dropHelper.Drop(pDataObj, ref pt, pdwEffect);

                return ShellAPI.S_OK;
            }

            ResetDrop();
            if (dropTarget != null)
            {
                dropTarget.DragDrop(pDataObj, grfKeyState, pt, ref pdwEffect);

                ReleaseCom();
                dropDataObject = IntPtr.Zero;
            }

            if (dropHelper != null)
                dropHelper.Drop(pDataObj, ref pt, pdwEffect);

            return ShellAPI.S_OK;
        }

        /// <summary>
        /// Reset all fields to the default values and release the IDropTarget
        /// </summary>
        private void ResetDrop()
        {
            if (dropListItem != null)
            {
                dropListItem.Selected = wasSelected;
                dropListItem = null;
                parentDropItem = null;
            }

            br.SelectionChange = true;
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// If not disposed, dispose the class
        /// </summary>
        public void Dispose()
        {
            if (!disposed)
            {
                DisposeDropWrapper();
                GC.SuppressFinalize(this);

                disposed = true;
            }
        }

        /// <summary>
        /// Revokes the ListView from getting drop messages and releases the IDropTarget
        /// </summary>
        private void DisposeDropWrapper()
        {
            ReleaseCom();

            if (dropHelper != null)
            {
                Marshal.ReleaseComObject(dropHelper);                
            }
        }

        /// <summary>
        /// Release the IDropTarget and free's the allocated memory
        /// </summary>
        private void ReleaseCom()
        {
            if (dropTarget != null)
            {
                Marshal.ReleaseComObject(dropTarget);

                dropTarget = null;
                dropHelperPtr = IntPtr.Zero;
            }
        }

        #endregion
    }

    #region Event Classes

    internal delegate void DropEventHandler(object sender, DropEventArgs e);

    internal class DropEventArgs : EventArgs
    {
        private ShellAPI.MK mouseButtons;
        private Control dragStartControl;

        public DropEventArgs(ShellAPI.MK mouseButtons, Control dragStartControl)
        {
            this.mouseButtons = mouseButtons;
            this.dragStartControl = dragStartControl;
        }

        public ShellAPI.MK MouseButtons { get { return mouseButtons; } }
        public Control DragStartControl { get { return dragStartControl; } }
    }

    #endregion
}
