#include "ctlspriv.h"

#define DF_ACTUALLYDRAG	0x0001
#define DF_DEFERRED	0x0002

#define INITLINESPERSECOND	6
#define VERTCHANGENUMLINES	25

#define TIMERID		238
#define TIMERLEN	50

#define DX_INSERT	16
#define DY_INSERT	16


typedef struct {
    HWND hwndDrag;
    UINT uFlags;
} DRAGPROP, *PDRAGPROP;

UINT uDragListMsg = 0;
#ifndef WINNT
#pragma data_seg(DATASEG_READONLY)
#endif
const TCHAR szDragListMsgString[] = DRAGLISTMSGSTRING;
#ifndef WINNT
#pragma data_seg()
#endif

BOOL NEAR PASCAL PtInLBItem(HWND hLB, int nItem, POINT pt, int xInflate, int yInflate)
{
  RECT rc;

  if (nItem < 0)
      nItem = (int)SendMessage(hLB, LB_GETCURSEL, 0, 0L);

  if (SendMessage(hLB, LB_GETITEMRECT, nItem, (LPARAM)(LPRECT)&rc) == LB_ERR)
      return(FALSE);

  InflateRect(&rc, xInflate, yInflate);

  return(PtInRect(&rc, pt));
}


/*
 * DragListSubclassProc
 * --------------------
 *
 * Window procedure for subclassed list boxes
 */
LRESULT CALLBACK DragListSubclassProc(HWND hLB, UINT uMsg, WPARAM wParam,
      LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
  PDRAGPROP pDragProp;
  DRAGLISTINFO sNotify;
  BOOL bDragging;
  POINT pt;

  pDragProp = (PDRAGPROP)dwRefData;
  bDragging = pDragProp->hwndDrag == hLB;

  switch (uMsg)
    {
      case WM_NCDESTROY:
        if (bDragging)
            SendMessage(hLB, WM_RBUTTONDOWN, 0, 0L);	/* cancel drag */

        RemoveWindowSubclass(hLB, DragListSubclassProc, 0);

        if (pDragProp)
            LocalFree((HLOCAL)pDragProp);
        break;

      case WM_LBUTTONDOWN:
        {
          int nItem;

          if (bDragging)				/* nested button-down */
              SendMessage(hLB, WM_RBUTTONDOWN, 0, 0L);	/* cancel drag */

          SetFocus(hLB);

          pt.x = GET_X_LPARAM(lParam);
          pt.y = GET_Y_LPARAM(lParam);
          ClientToScreen(hLB, &pt);
          nItem = LBItemFromPt(hLB, pt, FALSE);

          if (nItem >= 0)
            {
              SendMessage(hLB, LB_SETCURSEL, nItem, 0L);
              if (GetWindowLong(hLB, GWL_STYLE) & LBS_NOTIFY)
                  SendMessage(GetParent(hLB), WM_COMMAND,
                              GET_WM_COMMAND_MPS(GetDlgCtrlID(hLB), hLB, LBN_SELCHANGE));
              sNotify.uNotification = DL_BEGINDRAG;
              goto QueryParent;
            }
          else
              goto FakeDrag;
        }

      case WM_TIMER:
        if (wParam != TIMERID)
            break;
        lParam = GetMessagePosClient(hLB, &pt);


        // fall through
      case WM_MOUSEMOVE:
	if (bDragging)
	  {
	    HWND hwndParent;
	    LRESULT lResult;

	    /* We may be just simulating a drag, but not actually doing
	     * anything.
	     */
	    if (!(pDragProp->uFlags&DF_ACTUALLYDRAG))
		return(0L);

	    /* We don't want to do any dragging until the user has dragged
	     * outside of the current selection.
	     */
	    if (pDragProp->uFlags & DF_DEFERRED)
	      {
                pt.x = GET_X_LPARAM(lParam);
                pt.y = GET_Y_LPARAM(lParam);
		if (PtInLBItem(hLB, -1, pt, 0, 4))
		    return 0;
		pDragProp->uFlags &= ~DF_DEFERRED;
	      }

	    sNotify.uNotification = DL_DRAGGING;

QueryParent:
	    hwndParent = GetParent(hLB);
	    sNotify.hWnd = hLB;

            sNotify.ptCursor.x = GET_X_LPARAM(lParam);
            sNotify.ptCursor.y = GET_Y_LPARAM(lParam);
	    ClientToScreen(hLB, &sNotify.ptCursor);

            lResult = SendMessage(hwndParent, uDragListMsg, GetDlgCtrlID(hLB),
		  (LPARAM)(LPDRAGLISTINFO)&sNotify);

	    if (uMsg == WM_LBUTTONDOWN)
	      {
		/* Some things may not be draggable
		 */
		if (lResult)
		  {
		    SetTimer(hLB, TIMERID, TIMERLEN, NULL);
		    pDragProp->uFlags = DF_DEFERRED | DF_ACTUALLYDRAG;
		  }
		else
		  {
FakeDrag:
		    pDragProp->uFlags = 0;
		  }

		/* Set capture and change mouse cursor
		 */
		pDragProp->hwndDrag = hLB;

		SetCapture(hLB);
	      }
	    else
	      {
		switch (lResult)
		  {
		    case DL_STOPCURSOR:
                      SetCursor(LoadCursor(NULL, MAKEINTRESOURCE(IDC_NO)));
		      break;

		    case DL_COPYCURSOR:
                      SetCursor(LoadCursor(HINST_THISDLL, MAKEINTRESOURCE(IDC_COPY)));
		      break;

		    case DL_MOVECURSOR:
                      SetCursor(LoadCursor(HINST_THISDLL, MAKEINTRESOURCE(IDC_MOVE)));
		      break;

		    default:
		      break;
		  }
	      }

	    /* Don't call the def proc, since it may try to change the
	     * selection or set timers or things like that.
	     */
	    return(0L);
	  }
	break;

      case  WM_RBUTTONDOWN:
      case  WM_LBUTTONUP:
	/* if we are capturing mouse - release it and check for acceptable place
	 * where mouse is now to decide drop or not
	 */
	if (bDragging)
	  {
	    HWND hwndParent;

	    pDragProp->hwndDrag = NULL;
	    KillTimer(hLB, TIMERID);
	    ReleaseCapture();
	    SetCursor(LoadCursor(NULL, IDC_ARROW));

	    hwndParent = GetParent(hLB);

	    sNotify.uNotification = uMsg==WM_LBUTTONUP ? DL_DROPPED : DL_CANCELDRAG;
	    sNotify.hWnd = hLB;
            sNotify.ptCursor.x = GET_X_LPARAM(lParam);
            sNotify.ptCursor.y = GET_Y_LPARAM(lParam);
	    ClientToScreen(hLB, &sNotify.ptCursor);

	    SendMessage(hwndParent, uDragListMsg, GetDlgCtrlID(hLB),
		  (LPARAM)(LPDRAGLISTINFO)&sNotify);

	    /* We need to make sure to return 0 in case this is from a
	     * keyboard message.
	     */
	    return(0L);
	  }
	break;

      case WM_GETDLGCODE:
	if (bDragging)
          {
            return (DefSubclassProc(hLB, uMsg, wParam, lParam) |
                DLGC_WANTMESSAGE);
          }
	break;

      case WM_KEYDOWN:
        if (wParam == VK_ESCAPE)
          {
            SendMessage(hLB, WM_RBUTTONDOWN, 0, 0L);
          }
        // fall through
      case WM_CHAR:
      case WM_KEYUP:
	/* We don't want the listbox processing this if we are dragging.
	 */
	if (bDragging)
	    return(0L);
	break;

      default:
	break;
    }

  return(DefSubclassProc(hLB, uMsg, wParam, lParam));
}


BOOL WINAPI MakeDragList(HWND hLB)
{
  PDRAGPROP pDragProp;

  if (!uDragListMsg)
      uDragListMsg = RegisterWindowMessage(szDragListMsgString);

  /* Check that we have not already subclassed this window.
   */
  if (GetWindowSubclass(hLB, DragListSubclassProc, 0, NULL))
      return(TRUE);

  pDragProp = (PDRAGPROP)LocalAlloc(LPTR, sizeof(DRAGPROP));
  if (!pDragProp)
      return(FALSE);

  if (!SetWindowSubclass(hLB, DragListSubclassProc, 0, (DWORD_PTR)pDragProp))
  {
      LocalFree((HLOCAL)pDragProp);
      return(FALSE);
  }

  return(TRUE);
}


int WINAPI LBItemFromPt(HWND hLB, POINT pt, BOOL bAutoScroll)
{
  static LONG dwLastScroll = 0;

  RECT rc;
  DWORD dwNow;
  int nItem;
  WORD wScrollDelay, wActualDelay;

  ScreenToClient(hLB, &pt);
  GetClientRect(hLB, &rc);

  nItem = (int)SendMessage(hLB, LB_GETTOPINDEX, 0, 0L);

  /* Is the point in the LB client area?
   */
  if (PtInRect(&rc, pt))
    {
      /* Check each visible item in turn.
       */
      for ( ; ; ++nItem)
	{
	  if (SendMessage(hLB, LB_GETITEMRECT, nItem, (LPARAM)(LPRECT)&rc)
		== LB_ERR)
	      break;

	  if (PtInRect(&rc, pt))
	      return(nItem);
	}
    }
  else
    {
      /* If we want autoscroll and the point is directly above or below the
       * LB, determine the direction and if it is time to scroll yet.
       */
      if (bAutoScroll && (UINT)pt.x<(UINT)rc.right)
	{
	  if (pt.y <= 0)
	    {
	      --nItem;
	    }
	  else
	    {
	      ++nItem;
	      pt.y = rc.bottom - pt.y;
	    }
	  wScrollDelay = (WORD)(1000 /
		(INITLINESPERSECOND - pt.y/VERTCHANGENUMLINES));

	  dwNow = GetTickCount();
	  wActualDelay = (WORD)(dwNow - dwLastScroll);

	  if (wActualDelay > wScrollDelay)
	    {
	      /* This will the actual number of scrolls per second to be
	       * much closer to the required number.
	       */
	      if (wActualDelay > wScrollDelay * 2)
		  dwLastScroll = dwNow;
	      else
		  dwLastScroll += wScrollDelay;

	      SendMessage(hLB, LB_SETTOPINDEX, nItem, 0L);
	    }
	}
    }

  return(-1);
}


void WINAPI DrawInsert(HWND hwndParent, HWND hLB, int nItem)
{
  static POINT ptLastInsert;
  static int nLastInsert = -1;

  RECT rc;

  /* Erase the old mark if necessary
   */
  if (nLastInsert>=0 && nItem!=nLastInsert)
    {
      rc.left = ptLastInsert.x;
      rc.top = ptLastInsert.y;
      rc.right = rc.left + DX_INSERT;
      rc.bottom = rc.top + DY_INSERT;

      /* Need to update immediately in case the insert rects overlap.
       */
      InvalidateRect(hwndParent, &rc, TRUE);
      UpdateWindow(hwndParent);

      nLastInsert = -1;
    }

  /* Draw a new mark if necessary
   */
  if (nItem!=nLastInsert && nItem>=0)
    {
      HICON hInsert = NULL;

      if (!hInsert)
	  hInsert = LoadIcon(HINST_THISDLL, MAKEINTRESOURCE(IDI_INSERT));

      if (hInsert)
	{
	  HDC hDC;

	  GetWindowRect(hLB, &rc);
	  ScreenToClient(hLB, (LPPOINT)&rc);
	  ptLastInsert.x = rc.left - DX_INSERT;

	  SendMessage(hLB, LB_GETITEMRECT, nItem, (LPARAM)(LPRECT)&rc);
	  ptLastInsert.y = rc.top - DY_INSERT/2;

	  nLastInsert = nItem;

	  ClientToScreen(hLB, &ptLastInsert);
	  ScreenToClient(hwndParent, &ptLastInsert);

	  hDC = GetDC(hwndParent);
	  DrawIcon(hDC, ptLastInsert.x, ptLastInsert.y, hInsert);
	  ReleaseDC(hwndParent, hDC);
	}
    }
}