/*
    WDL - virtwnd-listbox.cpp
    Copyright (C) 2006 and later Cockos Incorporated

    This software is provided 'as-is', without any express or implied
    warranty.  In no event will the authors be held liable for any damages
    arising from the use of this software.

    Permission is granted to anyone to use this software for any purpose,
    including commercial applications, and to alter it and redistribute it
    freely, subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you must not
       claim that you wrote the original software. If you use this software
       in a product, an acknowledgment in the product documentation would be
       appreciated but is not required.
    2. Altered source versions must be plainly marked as such, and must not be
       misrepresented as being the original software.
    3. This notice may not be removed or altered from any source distribution.
      

    Implementation for virtual window listboxes.

*/

#include "virtwnd-controls.h"
#include "../lice/lice.h"

#define EXPAND_SCROLLBAR_SIZE(sz) ((sz)*2)

WDL_VirtualListBox::WDL_VirtualListBox()
{
  m_scrollbar_color = LICE_RGBA(0,128,255,255);
  m_scrollbar_blendmode=LICE_BLIT_MODE_COPY;
  m_scrollbar_alpha = 1.0f;
  m_scrollbar_size=4;
  m_scrollbar_border=1;
  m_scrollbar_expanded = false;
  m_want_wordwise_cols = false;
  m_cap_startitem=-1;
  m_cap_state=0;
  m_cap_startpos.x = m_cap_startpos.y = 0;
  m_margin_l=m_margin_r=0;
  m_GetItemInfo=0;
  m_CustomDraw=0;
  m_GetItemHeight=NULL;
  m_GetItemInfo_ctx=0;
  m_viewoffs=0;
  m_align=-1;
  m_rh=14;
  m_maxcolwidth=m_mincolwidth=0;
  m_colgap=0;
  m_lsadj=-1000;
  m_font=0;
  m_clickmsg=0;
  m_dropmsg=0;
  m_dragmsg=0;
  m_grayed=false;
}

WDL_VirtualListBox::~WDL_VirtualListBox()
{
}

static bool items_fit(int test_h, const int *heights, int heights_sz, int h, int max_cols, int rh_base)
{
  int y=test_h, cols=1;
  for (int item = 0; item < heights_sz; item ++)
  {
    int rh = heights[item] & WDL_VirtualListBox::ITEMH_MASK;
    if (y > 0 && y + rh > h) 
    {
      if (max_cols == 1 && !(heights[item]&WDL_VirtualListBox::ITEMH_FLAG_NOSQUISH) && 
            y + rh_base <= h) return item == heights_sz-1; // allow partial of larger item in single column mode
      if (++cols > max_cols) return false;
      y=0;
    }
    y += rh;
  }
  return true;
}

void WDL_VirtualListBox::CalcLayout(int num_items, layout_info *layout)
{
  const int w = m_position.right - m_position.left, h = m_position.bottom - m_position.top;

  int max_cols = 1, min_cols = 1, max_cols2 = 1;
  if (m_mincolwidth>0) 
  {
    max_cols = w / m_mincolwidth;
    max_cols2 = (w - m_scrollbar_size - m_colgap) / m_mincolwidth; // maximum column count when a scrollbar is visible
    if (max_cols < 1) max_cols = 1;
    if (max_cols2 < 1) max_cols2 = 1;
  }
  if (m_maxcolwidth>0)
  {
    min_cols = w / m_maxcolwidth;
    if (min_cols < 1) min_cols = 1;
  }
  if (max_cols < min_cols) max_cols = min_cols;
  if (max_cols2 < min_cols) max_cols2 = min_cols;

  static WDL_TypedBuf<int> s_heights;

again:
  s_heights.Resize(0,false);

  int maxvis = 1;
  if (!m_GetItemHeight)
  {
    const int rh = GetRowHeight();
    if (WDL_NORMALLY(rh>0)) maxvis = wdl_max(h/rh,1) * max_cols;
  }

  int startitem = wdl_min(m_viewoffs,num_items-maxvis), cols = 1, y = 0;
  if (startitem < 0) startitem = 0;

  const int rh_base = GetRowHeight();
  int item;
  for (item = startitem; item < num_items; item ++)
  {
    int flag=0;
    int rh = GetItemHeight(item, &flag);
    if (y > 0 && y + rh > h) 
    {
      if (cols == max_cols && !(flag&ITEMH_FLAG_NOSQUISH) && y + rh_base <= h) s_heights.Add(rh | flag); // allow partial of larger item in single column mode
      if (cols >= max_cols) break;
      cols++;
      y=0;
    }
    s_heights.Add(rh | flag);
    y += rh;
  }
  if (item >= num_items)
  {
    int use_col = wdl_max(min_cols,cols);
    if (m_GetItemHeight == NULL && m_want_wordwise_cols && use_col>1 && h >= rh_base*2)
    {
      int viscnt = use_col * (h/rh_base);
      if (startitem + viscnt > num_items + use_col-1)
      {
        startitem = num_items-viscnt + use_col-1;
        if (startitem < 0) startitem=0;
      }
    }
    else
    {
      int use_h = h;
      if (use_col > 1 && num_items >= use_col)
        use_h -= m_scrollbar_size;

      while (startitem > 0)
      {
        int flag=0;
        int rh = GetItemHeight(startitem-1, &flag);
        if (!items_fit(rh,s_heights.Get(),s_heights.GetSize(),use_h,max_cols,rh_base)) break;
        s_heights.Insert(rh | flag,0);
        startitem--;
      }
    }
  }
  const bool has_scroll = item < num_items || startitem > 0;

  if (has_scroll && cols > wdl_max(min_cols,1) && max_cols > max_cols2)
  {
    max_cols = max_cols2;
    goto again;
  }
  if (cols < min_cols) cols = min_cols;
  layout->startpos = startitem;
  layout->columns = cols;
  layout->heights = &s_heights;

  int scroll_mode = 0;
  if (has_scroll)
  {
    if (cols > 1 && (m_GetItemHeight!=NULL || !m_want_wordwise_cols || h < rh_base*2))
      scroll_mode=2;
    else
      scroll_mode=1;
  }

  layout->vscrollbar_w = scroll_mode == 1 ? m_scrollbar_size : 0;
  layout->hscrollbar_h = scroll_mode == 2 ? m_scrollbar_size : 0;
  layout->item_area_w = w - layout->vscrollbar_w;
  layout->item_area_h = wdl_max(h - layout->hscrollbar_h,rh_base);
  if (AreItemsWordWise(*layout))
  {
    int adj = layout->startpos%layout->columns;
    for (int x = 0; x <adj; x++)
      s_heights.Insert(rh_base,0);
    layout->startpos -= adj;
  }
  else if (scroll_mode == 2 && m_GetItemHeight == NULL && rh_base > 0)
  {
    const int rowcnt = layout->item_area_h / rh_base;
    if (rowcnt > 1)
    {
      int adj = rowcnt - (layout->startpos%rowcnt);
      if (adj < rowcnt)
      {
        if (adj > s_heights.GetSize()) adj = s_heights.GetSize();
        layout->startpos += adj;
        for (int x = 0; x < adj; x++) s_heights.Delete(0);
      }
    }
  }
}

static void maxSizePreservingAspect(int sw, int sh, int dw, int dh, int *outw, int *outh)
{
  *outw=dw;
  *outh=dh;
  if (sw < 1 || sh < 1) return;
  int xwid = (sw * dh) / sh; // calculate width required if we make it dh pixels tall
  if (xwid > dw)
  {
    // too wide, make maximum width and reduce height
    *outh = (dw * sh) / sw;
  }
  else 
  {
    // too narrow, use full height and reduce width
    *outw = (dh * sw) / sh;
  }
}

static void DrawBkImage(LICE_IBitmap *drawbm, WDL_VirtualWnd_BGCfg *bkbm, int drawx, int drawy, int draww, int drawh,
                RECT *cliprect, int drawsrcx, int drawsrcw, int bkbmstate, float alpha, int whichpass=0)
{
  bool haspink=bkbm->bgimage_lt[0]||bkbm->bgimage_lt[1]||bkbm->bgimage_rb[0] || bkbm->bgimage_rb[1];

  if (whichpass==1)
  {
    int sw = (bkbm->bgimage->getWidth() - (haspink? 2 :0))/2;
    int sh = (bkbm->bgimage->getHeight() - (haspink? 2 :0))/3;

    int usew,useh;
    // scale drawing coords by image dimensions
    maxSizePreservingAspect(sw,sh,draww,drawh,&usew,&useh);

    if (usew == sw-1 || usew == sw+1) usew=sw;
    if (useh == sh-1 || useh == sh+1) useh=sh;
    drawx += (draww-usew)/2;
    drawy += (drawh-useh)/2;
    draww = usew;
    drawh = useh;
  }

  int hh=bkbm->bgimage->getHeight()/3;

  if (haspink)
  {
    WDL_VirtualWnd_BGCfg tmp = *bkbm;
    if ((tmp.bgimage_noalphaflags&0xffff)!=0xffff) tmp.bgimage_noalphaflags=0;  // force alpha channel if any alpha

    if (drawsrcx>0) { drawsrcx--; drawsrcw++; }
    LICE_SubBitmap bm(tmp.bgimage,drawsrcx,bkbmstate*hh,drawsrcw+1,hh+2);
    tmp.bgimage = &bm;

    WDL_VirtualWnd_ScaledBlitBG(drawbm,&tmp,
                                  drawx,drawy,draww,drawh,
                                  cliprect->left,cliprect->top,cliprect->right-cliprect->left,cliprect->bottom-cliprect->top,
                                  alpha,LICE_BLIT_USE_ALPHA|LICE_BLIT_MODE_COPY|LICE_BLIT_FILTER_BILINEAR);
  }
  else
  {
    LICE_ScaledBlit(drawbm,bkbm->bgimage,
      drawx,drawy,draww,drawh,
      (float)drawsrcx,bkbmstate*(float)hh,
      (float)drawsrcw,(float)hh,alpha,LICE_BLIT_USE_ALPHA|LICE_BLIT_MODE_COPY|LICE_BLIT_FILTER_BILINEAR);
  }
}

int WDL_VirtualListBox::ScrollbarGetInfo(int *start, int *size, int num_items, const layout_info &layout)
{
  if (!layout.vscrollbar_w)
  {
    if (layout.hscrollbar_h)
    {
      const int total = wdl_max(num_items,1);
      const int cols = wdl_max(layout.columns,1);
      int vis = (layout.heights->GetSize() + cols - 1);
      vis -= (vis % cols);
      if (vis < cols) vis=cols;

      int startp = (layout.startpos * layout.item_area_w) / total;
      int endp = ((layout.startpos + vis)  * layout.item_area_w) / total;
      *start = startp;
      *size = endp-startp;

      if (*size<2) { if (*start>0) (*start)--; *size=2; }
      if (*start + *size > layout.item_area_w) *start = layout.item_area_w - *size;
      if (*start < 0) *start=0;

      return 2;
    }
    return 0;
  }

  const int num_cols = wdl_max(layout.columns,1);
  const int total_rows = wdl_max((num_items+num_cols-1) / num_cols,1);
  const int rh_base = wdl_max(GetRowHeight(),1);
  const int vis_rows =
    layout.columns > 1 ?  wdl_clamp(layout.item_area_h / rh_base,1,total_rows) :
      wdl_clamp(layout.heights->GetSize(),1,total_rows);

  const int srow = layout.startpos/num_cols;
  int startp = (srow * layout.item_area_h) / total_rows;
  int endp = ((srow + vis_rows)  * layout.item_area_h) / total_rows;
  *start = startp;
  *size = endp-startp;
  if (*size<2) { if (*start>0) (*start)--; *size=2; }
  if (*start + *size > layout.item_area_h) *start = layout.item_area_h - *size;
  if (*start < 0) *start=0;

  return 1;
}

void WDL_VirtualListBox::OnPaint(LICE_IBitmap *drawbm, int origin_x, int origin_y, RECT *cliprect, int rscale)
{
  RECT r;
  WDL_VWnd::GetPositionPaintExtent(&r,rscale);
  r.left+=origin_x;
  r.right+=origin_x;
  r.top+=origin_y;
  r.bottom+=origin_y;

  WDL_VirtualWnd_BGCfg *mainbk=0;
  int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,&mainbk) : 0;
  LICE_pixel bgc=GSC(COLOR_BTNFACE);
  bgc=LICE_RGBA_FROMNATIVE(bgc,255);

  layout_info layout;
  CalcLayout(num_items,&layout);
  m_viewoffs = layout.startpos; // latch to new startpos
  const int num_cols = layout.columns;
  const bool do_wordwise = AreItemsWordWise(layout);

  const int usedw = layout.item_area_w * rscale / WDL_VWND_SCALEBASE;
  const int vscrollsize = layout.vscrollbar_w * rscale / WDL_VWND_SCALEBASE;
  const int endpos=layout.item_area_h * rscale / WDL_VWND_SCALEBASE;
  const int startpos = m_viewoffs;
  const int rh_base = GetRowHeight();

  if (r.right > r.left + usedw+vscrollsize) r.right=r.left+usedw+vscrollsize;

  if (mainbk && mainbk->bgimage)
  {
    if (mainbk->bgimage->getWidth()>1 && mainbk->bgimage->getHeight()>1)
    {
      WDL_VirtualWnd_ScaledBlitBG(drawbm,mainbk,
                                    r.left,r.top,r.right-r.left,r.bottom-r.top,
                                    cliprect->left,cliprect->top,cliprect->right-cliprect->left,cliprect->bottom-cliprect->top,
                                    1.0,LICE_BLIT_USE_ALPHA|LICE_BLIT_MODE_COPY|LICE_BLIT_FILTER_BILINEAR);
    }
  }

  LICE_pixel pencol = GSC(COLOR_3DSHADOW);
  LICE_pixel pencol2 = GSC(COLOR_3DHILIGHT);
  pencol=LICE_RGBA_FROMNATIVE(pencol,255);
  pencol2=LICE_RGBA_FROMNATIVE(pencol2,255);

  LICE_pixel tcol=GSC(COLOR_BTNTEXT);
  if (m_font) 
  {
    m_font->SetBkMode(TRANSPARENT);
    if (m_lsadj != -1000)
      m_font->SetLineSpacingAdjust(m_lsadj);
  }

  float alpha = (m_grayed ? 0.25f : 1.0f);

  int itempos=startpos;
  
  for (int colpos = 0; colpos < (do_wordwise ? 1 : num_cols); colpos ++)
  {
    int col_x = r.left + ((usedw+m_colgap)*colpos) / num_cols;
    int col_w = r.left + ((usedw+m_colgap)*(colpos+1)) / num_cols - col_x - m_colgap;
    int y=r.top, ly = y;
    int cstate=0;
    for (;;)
    {
      const int idx = itempos-startpos;
      int rh,flag;
      if (idx >= 0 && idx < layout.heights->GetSize())
        rh = layout.GetHeight(idx,&flag);
      else
        rh = GetItemHeight(itempos,&flag);
      rh = rh * rscale / WDL_VWND_SCALEBASE;

      if (!do_wordwise)
      {
        ly=y;
        y += rh;
        if (y > r.top+endpos) 
        {
          if (ly != r.top)
          {
            if (y+ ((flag&ITEMH_FLAG_NOSQUISH) ? 0 : -rh + rh_base) > r.top+endpos) break;
            if (colpos < num_cols-1) break;
          }
          y = r.top+endpos; // size expanded-sized item to fit
        }
      }
      else
      {
        col_x = r.left + ((usedw+m_colgap)*cstate) / num_cols;
        col_w = r.left + ((usedw+m_colgap)*(cstate+1)) / num_cols - col_x - m_colgap;
        if (!cstate)
        {
          ly = y;
          y += rh;
          if (y > r.top+endpos)  break;
        }
        if (++cstate == num_cols) cstate=0;
      }

      WDL_VirtualWnd_BGCfg *bkbm=0;
      if (m_GetItemInfo && ly >= r.top)
      {
        char buf[64];
        buf[0]=0;
        int color=tcol;

        if (m_GetItemInfo(this,itempos,buf,sizeof(buf),&color,&bkbm))
        {
          color=LICE_RGBA_FROMNATIVE(color,0);
          RECT thisr;
          thisr.left = col_x;
          thisr.right = col_x + col_w;
          thisr.top = ly+1;
          thisr.bottom = y-1;
          int rev=0;
          int bkbmstate=0;
          if (m_cap_state==1 && m_cap_startitem==itempos)
          {
            if (bkbm) bkbmstate=1;
            else color = ((color>>1)&0x7f7f7f7f)+LICE_RGBA(0x7f,0x7f,0x7f,0);
          }
          if (m_cap_state>=0x1000 && m_cap_startitem==itempos)
          {
            if (bkbm) bkbmstate=2;
            else
            {
              rev=1;
              LICE_FillRect(drawbm,thisr.left,thisr.top,thisr.right-thisr.left,thisr.bottom-thisr.top, color,alpha,LICE_BLIT_MODE_COPY);
            }
          }
          if (bkbm && bkbm->bgimage) //draw image!
          {            
            DrawBkImage(drawbm,bkbm,
                thisr.left,thisr.top-1,thisr.right-thisr.left,thisr.bottom-thisr.top+2,
                cliprect,
                0,bkbm->bgimage->getWidth(),bkbmstate,alpha);

          }
          if (m_CustomDraw)
          {
            m_CustomDraw(this,itempos,&thisr,drawbm,rscale);
          }

          if (buf[0])
          {
            thisr.left+=m_margin_l;
            thisr.right-=m_margin_r;
            if (m_font)
            {
              m_font->SetTextColor(rev?bgc:color);
              m_font->SetCombineMode(LICE_BLIT_MODE_COPY, alpha); // maybe gray text only if !bkbm->bgimage
              RECT dr=thisr;
              const bool has_nl = strchr(buf,'\n') != NULL;
              RECT r2 = { 0, };
              if (has_nl || m_align == 0)
                m_font->DrawText(drawbm,buf,-1,&r2,DT_CALCRECT|DT_NOPREFIX);

              int f = (m_align > 0 ? DT_RIGHT : ((m_align == 0 && r2.right <= thisr.right-thisr.left) ? DT_CENTER : DT_LEFT));

              if (has_nl)
              {
                // can't use DT_VCENTER
                OffsetRect(&dr,0,((dr.bottom-dr.top) - (r2.bottom-r2.top))/2);
              }
              else
              {
                f |= DT_SINGLELINE | DT_VCENTER;
              }

              m_font->DrawText(drawbm,buf,-1,&dr,f | DT_NOPREFIX);
            }
          }
        }
      }
      itempos++;

      if (!bkbm)
      {
        LICE_Line(drawbm,col_x,y,col_x+col_w,y,pencol2,1.0f,LICE_BLIT_MODE_COPY,false);
      }
    }
  }
  if (num_cols>0)
  {
    int offs, height;

    int stype = ScrollbarGetInfo(&offs,&height,num_items,layout);
    if (stype)
    {
      if (rscale != 256)
      {
        offs = offs*rscale / WDL_VWND_SCALEBASE;
        height = height*rscale / WDL_VWND_SCALEBASE;
      }

      int w = stype == 1 ? vscrollsize : (layout.hscrollbar_h * rscale / WDL_VWND_SCALEBASE);
      int drawposx = stype == 1 ? (r.left + usedw) : (r.top + endpos);
      if (m_scrollbar_expanded)
      {
        drawposx -= EXPAND_SCROLLBAR_SIZE(w);
        w += EXPAND_SCROLLBAR_SIZE(w);
      }
      else
      {
        int border = m_scrollbar_border * rscale / WDL_VWND_SCALEBASE;
        if (border>0 && border < w)
        {
          w-=border;
          drawposx+=border;
        }
      }

      const LICE_pixel col = m_scrollbar_color;
      const int mode = m_scrollbar_blendmode;
      const float alpha = m_scrollbar_alpha;
      if (stype == 1)
      {
        LICE_FillRect(drawbm,drawposx,r.top,w,endpos, col,alpha*.5f,mode);
        LICE_FillRect(drawbm,drawposx,r.top + offs,w,height, col,alpha,mode);
      }
      else
      {
        if (drawposx+w > r.bottom) w = r.bottom - drawposx;
        LICE_FillRect(drawbm,r.left,drawposx,usedw,w, col,alpha*.5f,mode);
        LICE_FillRect(drawbm,r.left+offs,drawposx,height,w, col,alpha,mode);
      }
    }
  }

  if (!mainbk)
  {
    LICE_Line(drawbm,r.left,r.bottom-1,r.left,r.top,pencol,1.0f,0,false);
    LICE_Line(drawbm,r.left,r.top,r.right-1,r.top,pencol,1.0f,0,false);
    LICE_Line(drawbm,r.right-1,r.top,r.right-1,r.bottom-1,pencol2,1.0f,0,false);
    LICE_Line(drawbm,r.right-1,r.bottom-1,r.left,r.bottom-1,pencol2,1.0f,0,false);
  }


}
void WDL_VirtualListBox::DoScroll(int dir, const layout_info *layout)
{
  if (dir < 0 && layout->columns>1)
  {
    int y=0;
    if (AreItemsWordWise(*layout))
    {
      int np = wdl_max(0,m_viewoffs-layout->columns);
      if (m_viewoffs != np) { m_viewoffs = np; y=1; }
    }
    else while (m_viewoffs>0)
    {
      y += GetItemHeight(--m_viewoffs);
      if (y >= layout->item_area_h) break;
    }
    if (y) RequestRedraw(NULL);
  }
  else if (dir > 0 && layout->columns>1)
  {
    int y=0;
    if (AreItemsWordWise(*layout))
    {
      m_viewoffs+=layout->columns;
      y++;
    }
    else for (int i = 0;  i < layout->heights->GetSize(); i ++)
    {
      m_viewoffs++;
      y += layout->GetHeight(i);
      if (y >= layout->item_area_h) break;
    }
    if (y)
      RequestRedraw(NULL);
  }
  else if (dir > 0)
  {
    m_viewoffs++; // let painting sort out total visibility (we could easily calculate but meh)
    RequestRedraw(NULL);
  }
  else if (dir < 0)
  {
    if (m_viewoffs>0)
    {
      m_viewoffs--;
      RequestRedraw(NULL);
    }
  }
}

int WDL_VirtualListBox::OnMouseDown(int xpos, int ypos)
{
  if (m_grayed) return 0;
  m_cap_startpos.x = xpos;
  m_cap_startpos.y = ypos;

  if (m__iaccess) m__iaccess->OnFocused();
  const int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
  layout_info layout;
  CalcLayout(num_items,&layout);

  if (ScrollbarHit(xpos,ypos,layout))
  {
    int offs,sz;
    switch (ScrollbarGetInfo(&offs,&sz,num_items,layout))
    {
      case 1:
        if (ypos < offs || ypos > offs+sz)
        {
          m_viewoffs = (num_items * (ypos - sz/2)) / wdl_max(1,layout.item_area_h);
          if (m_viewoffs<0) m_viewoffs=0;
          if (m_viewoffs>num_items) m_viewoffs=num_items;
          m_cap_startpos.y = sz/2;
        }
        else
          m_cap_startpos.y = ypos - offs;

        m_scrollbar_expanded = true;
        m_cap_state=2;
        RequestRedraw(NULL);
      return 1;
      case 2:
        if (xpos < offs || xpos > offs+sz)
        {
          m_viewoffs = (num_items * (xpos - sz/2)) / wdl_max(1,layout.item_area_w);
          if (m_viewoffs<0) m_viewoffs=0;
          if (m_viewoffs>num_items) m_viewoffs=num_items;
          m_cap_startpos.x = sz/2;
        }
        else
          m_cap_startpos.x = xpos - offs;

        m_scrollbar_expanded = true;
        m_cap_state=3;
        RequestRedraw(NULL);
      return 1;
    }
  }

  int idx = IndexFromPtInt(xpos,ypos,layout);
  if (idx < 0)
    return 0;

  m_cap_state=0x1000;
  m_cap_startitem=idx;
  RequestRedraw(NULL);
  return 1;
}


bool WDL_VirtualListBox::OnMouseDblClick(int xpos, int ypos)
{
  if (m_grayed) return false;

  const int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
  layout_info layout;
  CalcLayout(num_items,&layout);

  int idx = IndexFromPtInt(xpos,ypos,layout);
  if (idx<0)
  {
    if (idx == -2) return false;
    idx = num_items;
  }
  
  RequestRedraw(NULL);
  if (m_clickmsg) SendCommand(m_clickmsg,(INT_PTR)this,idx,this);
  return false;
}

bool WDL_VirtualListBox::OnMouseWheel(int xpos, int ypos, int amt)
{
  if (m_grayed) return false;

  const int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
  layout_info layout;
  CalcLayout(num_items,&layout);

  if (xpos >= layout.item_area_w + layout.vscrollbar_w) return false;

  DoScroll(-amt,&layout);
  return true;
}

void WDL_VirtualListBox::OnMouseMove(int xpos, int ypos)
{
  if (m_grayed) return;

  if (m_cap_state>=0x1000)
  {
    m_cap_state++;
    if (m_cap_state < 0x1008)
    {
      int dx = (xpos - m_cap_startpos.x), dy=(ypos-m_cap_startpos.y);
      if (dx*dx + dy*dy > 36) 
        m_cap_state=0x1008;
    }
    if (m_cap_state>=0x1008)
    {
      if (m_dragmsg)
      {
        SendCommand(m_dragmsg,(INT_PTR)this,m_cap_startitem,this);
      }
    }
  }
  else if (m_cap_state >= 0 && m_cap_state <= 3)
  {
    const int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
    layout_info layout;
    CalcLayout(num_items,&layout);
    if (m_cap_state==0)
    {
      int a=IndexFromPtInt(xpos,ypos,layout);
      if (a>=0)
      {
        m_cap_startitem=a;
        m_cap_state=1;
        RequestRedraw(NULL);
      }
    }
    else if (m_cap_state==1)
    {
      int a=IndexFromPtInt(xpos,ypos,layout);
      if (a>=0 && a != m_cap_startitem)
      {
        m_cap_startitem=a;
        m_cap_state=1;
        RequestRedraw(NULL);
      }
      else if (a<0)
      {
        m_cap_state=0;
        RequestRedraw(NULL);
      }
    }
    else if (m_cap_state==2)
    {
      int vwoffs = (num_items * (ypos - m_cap_startpos.y)) / wdl_max(1,layout.item_area_h);
      if (vwoffs<0) vwoffs=0;
      if (vwoffs>num_items) vwoffs=num_items;
      if (vwoffs != m_viewoffs)
      {
        m_viewoffs = vwoffs;
        RequestRedraw(NULL);
      }
    }
    else if (m_cap_state==3)
    {
      int vwoffs = (num_items * (xpos - m_cap_startpos.x)) / wdl_max(1,layout.item_area_w);
      if (vwoffs<0) vwoffs=0;
      if (vwoffs>num_items) vwoffs=num_items;
      if (vwoffs != m_viewoffs)
      {
        m_viewoffs = vwoffs;
        RequestRedraw(NULL);
      }
    }
    bool exp = (m_cap_state>=2 || ScrollbarHit(xpos,ypos,layout));
    if (exp != m_scrollbar_expanded)
    {
      m_scrollbar_expanded = exp;
      RequestRedraw(NULL);
    }
  }
}

bool WDL_VirtualListBox::ScrollbarHit(int xpos, int ypos, const layout_info &layout)
{
  if (layout.vscrollbar_w>0)
  {
    int extra = m_scrollbar_expanded ? EXPAND_SCROLLBAR_SIZE(layout.vscrollbar_w) : 0;
    return ypos>=0 && ypos < layout.item_area_h &&
           xpos >= layout.item_area_w - extra && xpos < layout.item_area_w + layout.vscrollbar_w;
  }
  else if (layout.hscrollbar_h>0)
  {
    int extra = m_scrollbar_expanded ? EXPAND_SCROLLBAR_SIZE(layout.hscrollbar_h) : 0;
    return xpos>=0 && xpos < layout.item_area_w &&
           ypos >= layout.item_area_h - extra && ypos < layout.item_area_h + layout.hscrollbar_h;
  }
  return false;
}

void WDL_VirtualListBox::OnMouseUp(int xpos, int ypos)
{
  if (m_grayed) return;

  const int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
  layout_info layout;
  CalcLayout(num_items,&layout);

  int cmd=0;
  INT_PTR p1, p2;
  int hit=IndexFromPtInt(xpos,ypos,layout);
  if (m_cap_state>=0x1000 && m_cap_state<0x1008 && hit==m_cap_startitem) 
  {
    if (m_clickmsg)
    {
      cmd=m_clickmsg;
      p1=(INT_PTR)this;
      p2=hit;
    }
  }
  else if (m_cap_state>=0x1008)
  {
    // send a message saying drag & drop occurred
    if (m_dropmsg)
    {
      cmd=m_dropmsg;
      p1=(INT_PTR)this;
      p2=m_cap_startitem;
    }
  }
  else if (m_cap_state == 2 || m_cap_state == 3)
  {
    m_scrollbar_expanded = ScrollbarHit(xpos,ypos,layout);
  }

  m_cap_state=0;
  RequestRedraw(NULL);
  if (cmd) SendCommand(cmd,p1,p2,this);
}

bool WDL_VirtualListBox::GetItemRect(int item, RECT *r)
{
  const int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
  layout_info layout;
  CalcLayout(num_items,&layout);

  item -= layout.startpos;
  if (item < 0) { if (r) memset(r,0,sizeof(RECT)); return false; }
  
  if (r)
  {
    const bool do_wordwise = AreItemsWordWise(layout);
    const int rh_base = GetRowHeight();
    int col = 0,y=0;
    for (int x=0;x<item;x++)
    {
      if (do_wordwise)
      {
        if (++col == layout.columns)
        {
          y += rh_base;
          col = 0;
        }
      }
      else
      {
        int flag = 0;
        const int rh = x < layout.heights->GetSize() ? layout.GetHeight(x,&flag) : rh_base;
        if (y > 0 && y + rh > layout.item_area_h && (col < layout.columns-1 || (flag&ITEMH_FLAG_NOSQUISH) || y+rh_base > layout.item_area_h)) { col++; y = 0; }
        y += rh;
      }
    }

    int flag = 0;
    const int rh = item < layout.heights->GetSize() ? layout.GetHeight(item,&flag) : rh_base;

    if (!do_wordwise)
    {
      if (y > 0 && y + rh > layout.item_area_h && (col < layout.columns-1 || (flag&ITEMH_FLAG_NOSQUISH) || y+rh_base > layout.item_area_h)) { col++; y = 0; }
    }
    if (col >= layout.columns)  { if (r) memset(r,0,sizeof(RECT)); return false; }

    r->top = y;
    r->bottom = y+rh;
    if (r->bottom > layout.item_area_h) r->bottom = layout.item_area_h;
    r->left = (col * layout.item_area_w) / layout.columns;
    r->right = ((col+1) * layout.item_area_w) / layout.columns;
    if (col == layout.columns-1 && layout.vscrollbar_w && m_scrollbar_expanded)
    {
      r->right -= EXPAND_SCROLLBAR_SIZE(layout.vscrollbar_w); // exclude the expanded-scrollbar area from the rect
    }
  }
  return true;
}

int WDL_VirtualListBox::IndexFromPt(int x, int y)
{
  const int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
  layout_info layout;
  CalcLayout(num_items,&layout);
  return IndexFromPtInt(x,y,layout);
}

int WDL_VirtualListBox::IndexFromPtInt(int x, int y, const layout_info &layout)
{
  if (x >= layout.item_area_w - (m_scrollbar_expanded ? EXPAND_SCROLLBAR_SIZE(layout.vscrollbar_w) : 0)) return -2;

  if (y < 0 || y >= layout.item_area_h || x < 0)
  {
    return -1;
  }

  // step through visible items
  const int usewid=layout.item_area_w;
  int xpos = 0;
  int col = 0;

  const int do_wordwise = AreItemsWordWise(layout);
  const int rh_base = GetRowHeight();
  int idx = 0;

  if (do_wordwise)
  {
    int ypos = 0;
    for (;;)
    {
      const int nx = (++col * usewid) / layout.columns;
      int flag = 0;
      const int rh = idx < layout.heights->GetSize() ? layout.GetHeight(idx,&flag) : rh_base;
      if (x < nx && y >= ypos && y < ypos+rh) return layout.startpos + idx;
      if (col == layout.columns)
      {
        col = 0;
        ypos += rh;
      }
      idx++;
    }
  }
  else for (;;)
  {
    if (x < xpos) return -1;
    int ypos = 0;
    const int nx = ((col+1) * usewid) / layout.columns;
    for (;;)
    {
      int flag = 0;
      const int rh = idx < layout.heights->GetSize() ? layout.GetHeight(idx,&flag) : rh_base;
      if (ypos > 0 && ypos + rh > layout.item_area_h && (col < layout.columns-1 || (flag & ITEMH_FLAG_NOSQUISH) || ypos+rh_base > layout.item_area_h))
      {
        // item doesn't fit in this column
        if (x < nx) return -1; // stop looking we didn't hit anything
        break;
      }
      if (x < nx && y >= ypos && y < ypos+rh) return layout.startpos + idx;
      ypos += rh;
      idx++;
    }
    if (++col >= layout.columns) return -1;
    if (x < nx && y >= ypos) return -1; // empty space at bottom of column
    xpos = nx;
  }
}

void WDL_VirtualListBox::SetViewOffset(int offs)
{
  m_viewoffs = wdl_max(offs,0);
  RequestRedraw(0);
}

int WDL_VirtualListBox::GetViewOffset()
{
  return m_viewoffs;
}

int WDL_VirtualListBox::GetItemHeight(int idx, int *flag)
{
  const int a = m_GetItemHeight ? m_GetItemHeight(this,idx) : -1;
  if (flag) *flag = a<0 ? 0 : (a&~ITEMH_MASK);
  return a>=0 ? (a&ITEMH_MASK) : GetRowHeight();
}

RECT *WDL_VirtualListBox::GetScrollButtonRect(bool isDown)
{
  // used for accessibility!
  static RECT ret;

  int num_items = m_GetItemInfo ? m_GetItemInfo(this,-1,NULL,0,NULL,NULL) : 0;
  layout_info layout;
  CalcLayout(num_items,&layout);

  int start,sz;
  int sb = ScrollbarGetInfo(&start,&sz,num_items,layout);

  if (sb == 1 && layout.vscrollbar_w>0)
  {
    if (isDown && start+sz >= layout.item_area_h) return NULL;
    if (!isDown && start <= 0) return NULL;

    ret.left = layout.item_area_w;
    ret.right = ret.left + layout.vscrollbar_w;
    ret.top = isDown ? start+sz : start - layout.vscrollbar_w;
    ret.bottom = ret.top + layout.vscrollbar_w;
    return &ret;
  }
  if (sb ==2 && layout.hscrollbar_h>0)
  {
    if (isDown && start+sz >= layout.item_area_w) return NULL;
    if (!isDown && start <= 0) return NULL;
    ret.top = layout.item_area_h;
    ret.bottom = ret.top + layout.hscrollbar_h;
    ret.left = isDown ? start+sz : start - layout.hscrollbar_h;
    ret.right = ret.left + layout.hscrollbar_h;
    return &ret;
  }

  return NULL;
}
