// =======================================================================
// GUIslice library (extensions)
// - Calvin Hass
// - https://www.impulseadventure.com/elec/guislice-gui.html
// - https://github.com/ImpulseAdventure/GUIslice
// =======================================================================
//
// The MIT License
//
// Copyright 2016-2020 Calvin Hass
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// =======================================================================
/// \file XListbox.c



// GUIslice library
#include "GUIslice.h"
#include "GUIslice_drv.h"

#include "XListbox.h"

#include <stdio.h>

#if (GSLC_USE_PROGMEM)
    #include <avr/pgmspace.h>
#endif

// ----------------------------------------------------------------------------
// Error Messages
// ----------------------------------------------------------------------------

extern const char GSLC_PMEM ERRSTR_NULL[];
extern const char GSLC_PMEM ERRSTR_PXD_NULL[];


// ----------------------------------------------------------------------------
// Extended element definitions
// ----------------------------------------------------------------------------
//
// - This file extends the core GUIslice functionality with
//   additional widget types

// ----------------------------------------------------------------------------

// TODO: Combine with GUIslice MAX_STR
// Defines the maximum length of a listbox item
#define XLISTBOX_MAX_STR        20

// ============================================================================
// Extended Element: Listbox
// - A Listbox control
// ============================================================================


// Basic debug functionality
/*
void debug_ElemXListboxDump(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef)
{
  gslc_tsElem*      pElem = gslc_GetElemFromRef(pGui, pElemRef);
  gslc_tsXListbox*  pListbox = (gslc_tsXListbox*)(pElem->pXData);

  // Prevent runaway looping due to damaged buffer
  // Don't ask why I know to do this - PaulC

  int16_t    nSafety=0; 
  char acTxt[50 + 1];
  GSLC_DEBUG2_PRINT("Xlistbox:Dump (nBufPos=%d nItemCnt=%d)\n", pListbox->nBufItemsPos,pListbox->nItemCnt);
  for (unsigned nInd = 0; nInd < pListbox->nBufItemsPos; nInd++) {
    nSafety++;
    if (nSafety > pListbox->nBufItemsMax) break;
    char ch = pListbox->pBufItems[nInd];
    if (ch == 0) {
      snprintf(acTxt, 40,"0x%04x: [%2u] = %02X = [NULL]", (char*)&pListbox->pBufItems[nInd],nInd, ch);
    }
    else {
      snprintf(acTxt, 40,"0x%04x: [%2u] = %02X = [%c]", (char*)&pListbox->pBufItems[nInd],nInd, ch, ch);
    }
    GSLC_DEBUG2_PRINT("XListbox:Dump: %s\n", acTxt);
  }
}
*/

char* gslc_ElemXListboxGetItemAddr(gslc_tsXListbox* pListbox, int16_t nItemCurSel)
{
  char*      pBuf = NULL;
  uint16_t   nBufPos = 0;
  uint16_t   nItemInd = 0;
  bool       bFound = false;
  while (1) {
    if (nItemInd == nItemCurSel) {
      bFound = true;
      break;
    }
    if (nBufPos >= pListbox->nBufItemsMax) {
      break;
    }
    if (pListbox->pBufItems[nBufPos] == 0) {
      nItemInd++;
    }
    nBufPos++;
  }
  if (bFound) {
    pBuf = (char*)&(pListbox->pBufItems[nBufPos]);
    return pBuf;
  } else {
    return NULL;
  }
}

// Recalculate listbox item sizing if enabled
bool gslc_ElemXListboxRecalcSize(gslc_tsXListbox* pListbox,gslc_tsRect rElem)
{
  int16_t nElem;
  int16_t nElemInner;
  int16_t nItemOuter;
  int16_t nItemWOld;
  bool bNeedRedraw = false;

  // If number of rows was auto-sized based on content, then calculate now
  if (pListbox->nRows == XLISTBOX_SIZE_AUTO) {
    if (pListbox->nItemCnt == 0) {
      pListbox->nRows = 1; // Force at least one row
    } else {
      pListbox->nRows = ((pListbox->nItemCnt + 1) / pListbox->nCols);
    }
  }

  // NOTE: In the nElemInner calculation, we add nItemGap to account
  // for the fact that the last column does not include a "gap" after it.
  if (pListbox->bItemAutoSizeW) {
    nItemWOld = pListbox->nItemW;
    nElem = rElem.w;
    nElemInner = nElem - (2 * pListbox->nMarginW) + pListbox->nItemGap;
    nItemOuter = nElemInner / pListbox->nCols;
    pListbox->nItemW = nItemOuter - pListbox->nItemGap;
    if (pListbox->nItemW != nItemWOld) {
      bNeedRedraw = true;
    }
  }
  if (pListbox->bItemAutoSizeH) {
    nItemWOld = pListbox->nItemH;
    nElem = rElem.h;
    nElemInner = nElem - (2 * pListbox->nMarginH) + pListbox->nItemGap;
    nItemOuter = nElemInner / pListbox->nRows;
    pListbox->nItemH = nItemOuter - pListbox->nItemGap;
    if (pListbox->nItemH != nItemWOld) {
      bNeedRedraw = true;
    }
  }

  // Clear Need Recalc flag
  pListbox->bNeedRecalc = false;

  return bNeedRedraw;
}

void gslc_ElemXListboxSetSize(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, int8_t nRows, int8_t nCols)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return;

  pListbox->nRows = nRows;
  pListbox->nCols = nCols;
  pListbox->bNeedRecalc = true;
  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);
}



void gslc_ElemXListboxSetMargin(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, int8_t nMarginW, int8_t nMarginH)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return;

  pListbox->nMarginW = nMarginW;
  pListbox->nMarginH = nMarginH;
  pListbox->bNeedRecalc = true;
  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);
}


void gslc_ElemXListboxItemsSetSize(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, int16_t nItemW, int16_t nItemH)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return;

  pListbox->nItemW = nItemW;
  pListbox->nItemH = nItemH;
  // Determine if auto-sizing requested
  pListbox->bItemAutoSizeW = (nItemW == XLISTBOX_SIZE_AUTO) ? true : false;
  pListbox->bItemAutoSizeH = (nItemH == XLISTBOX_SIZE_AUTO) ? true : false;

  pListbox->bNeedRecalc = true;
  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);
}

void gslc_ElemXListboxItemsSetGap(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, int8_t nGap, gslc_tsColor colGap)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return;

  pListbox->nItemGap = nGap;
  pListbox->colGap = colGap;
  pListbox->bNeedRecalc = true;
  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);
}


void gslc_ElemXListboxReset(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return;

  pListbox->nBufItemsPos = 0;
  pListbox->nItemCnt = 0;
  pListbox->nItemCurSel = XLISTBOX_SEL_NONE;
  pListbox->bNeedRecalc = true;
  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);
}


bool gslc_ElemXListboxAddItem(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, const char* pStrItem)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  int8_t      nStrItemLen;
  char*       pBuf = NULL;
  uint16_t    nBufItemsPos = pListbox->nBufItemsPos;
  uint16_t    nBufItemsMax = pListbox->nBufItemsMax;

  nStrItemLen = strlen(pStrItem);

  if (nStrItemLen == 0) {
    // Nothing to add
    return false;
  }

  // Ensure we won't overrun the buffer, including the terminator
  if (nBufItemsPos + nStrItemLen + 1 > nBufItemsMax) {
    // Proceed with truncation
    nStrItemLen = nBufItemsMax - pListbox->nBufItemsPos - 1;
    GSLC_DEBUG2_PRINT("ERROR: ElemXListboxAddItem() buffer too small\n", "");
  }

  // If the truncation left nothing to add, skip out now
  if (nStrItemLen <= 0) {
    return false;
  }

  // Treat this section of the buffer as [char]
  pBuf = (char*)pListbox->pBufItems + nBufItemsPos;
  strncpy(pBuf, pStrItem, nStrItemLen);
  pListbox->nBufItemsPos += nStrItemLen;

  // Ensure terminator added
  pBuf += nStrItemLen;
  *pBuf = 0;
  pListbox->nBufItemsPos++;

  pListbox->nItemCnt++;

  //GSLC_DEBUG2_PRINT("Xlistbox:Add\n", "");
  //debug_ElemXListboxDump(pGui, pElemRef);

  // Inidicate sizing may need update
  pListbox->bNeedRecalc = true;

  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);
  return true;
}

bool gslc_ElemXListboxInsertItemAt(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, uint16_t nInsertPos, 
  const char* pStrItem)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  int8_t      nStrItemLen;
  char*       pBuf = NULL;
  uint16_t    nBufItemsPos = pListbox->nBufItemsPos;
  uint16_t    nBufItemsMax = pListbox->nBufItemsMax;

  if (nInsertPos > pListbox->nItemCnt) {
    GSLC_DEBUG2_PRINT("ERROR: ElemXListboxInsertItemAt() Current Count: %d Invalid Position %d\n", 
      pListbox->nItemCnt,nInsertPos);
    return false;
  }
  nStrItemLen = strlen(pStrItem);

  if (nStrItemLen == 0) {
    // Nothing to add
    return false;
  }

  // Ensure we won't overrun the buffer, including the terminator
  if (nBufItemsPos + nStrItemLen + 1 > nBufItemsMax) {
    GSLC_DEBUG2_PRINT("ERROR: ElemXListboxInsertItemAt() buffer too small\n", "");
    return false;
  }

  // If the truncation left nothing to add, skip out now
  if (nStrItemLen <= 0) {
    return false;
  }

//  GSLC_DEBUG2_PRINT("Xlistbox:InsertAt: %d\n", nInsertPos);
//  debug_ElemXListboxDump(pGui, pElemRef);

  pBuf = gslc_ElemXListboxGetItemAddr(pListbox, nInsertPos);
  // If position is incorrect, bail out...
  if (pBuf == NULL) {
    return false;
  }
 
  // Make a hole in the buffer to slot in the new item
  char* pSrc = (char*)pListbox->pBufItems+nBufItemsPos-1;
  char* pDest = pSrc+nStrItemLen+1;  
  int16_t nMoveLen = (int16_t)(pSrc - pBuf)+1;
  uint8_t ch;
  for (uint16_t nInd = 0; nInd < nMoveLen; nInd++) {
    ch = *pSrc;
    *pDest = ch;
    pDest--;
    pSrc--;
  }
  
  // Now slot in the new item
  memcpy(pBuf, pStrItem, nStrItemLen+1);

  // update our buffer information
  pListbox->nBufItemsPos += nStrItemLen+1;
  pListbox->nItemCnt++;

//  GSLC_DEBUG2_PRINT("Xlistbox:After InsertAt %d\n", nInsertPos);
//  debug_ElemXListboxDump(pGui, pElemRef);

  // Indicate sizing may need update
  pListbox->bNeedRecalc = true;

  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);

  return true;
}

bool gslc_ElemXListboxDeleteItemAt(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, uint16_t nDeletePos)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  int8_t      nStrItemLen;
  char*       pBuf = NULL;
  uint16_t    nBufItemsPos = pListbox->nBufItemsPos;

//  GSLC_DEBUG2_PRINT("Xlistbox:DeleteAt: %d\n", nDeletePos);
//  debug_ElemXListboxDump(pGui, pElemRef);

  pBuf = gslc_ElemXListboxGetItemAddr(pListbox, nDeletePos);
  // If position is incorrect, bail out...
  if (pBuf == NULL) {
    return false;
  }
  nStrItemLen = strlen(pBuf);
  
  // Pull items after this delete position up to delete this item
  char* pSrc  = (char*)pBuf+nStrItemLen+1;
  char* pDest = (char*)pBuf;
  char* pEndOfBuf = (char*)(pListbox->pBufItems+nBufItemsPos);
  int16_t nMoveLen = (int16_t)(pEndOfBuf-pSrc)+1;
  uint8_t ch;
  if (nMoveLen > 1) {
    for (uint16_t nInd = 0; nInd < nMoveLen; nInd++) {
      ch = *pSrc;
      *pDest = ch;
      pDest++;
      pSrc++;
    }
  }
  
  // update our buffer information
  pListbox->nBufItemsPos -= nStrItemLen+1;
  pListbox->nItemCnt--;
  
  // unselect item
  gslc_ElemXListboxSetSel(pGui, pElemRef, XLISTBOX_SEL_NONE);

//  GSLC_DEBUG2_PRINT("Xlistbox:After DeleteAt %d\n", nDeletePos);
//  debug_ElemXListboxDump(pGui, pElemRef);

  // Indicate sizing may need update
  pListbox->bNeedRecalc = true;

  // Mark as needing full redraw
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);

  return true;
}

bool gslc_ElemXListboxGetItem(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, int16_t nItemCurSel, char* pStrItem, 
  uint8_t nStrItemLen)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  // Ensure user provided valid string
  if ((pStrItem == NULL) || (nStrItemLen == 0)) {
    // ERROR
    return false;
  }
  uint16_t   nBufPos = 0;
  uint16_t   nItemInd = 0;
  uint8_t*   pBuf = NULL;
  bool       bFound = false;
  while (1) {
    if (nItemInd == nItemCurSel) {
      bFound = true;
      break;
    }
    if (nBufPos >= pListbox->nBufItemsMax) {
      break;
    }
    if (pListbox->pBufItems[nBufPos] == 0) {
      nItemInd++;
    }
    nBufPos++;
  }
  if (bFound) {
    pBuf = &(pListbox->pBufItems[nBufPos]);
    strncpy(pStrItem, (char*)pBuf, nStrItemLen);
    // Ensure null terminated in case the buffer
    // was smaller than the item source
    pStrItem[nStrItemLen - 1] = 0;
    return true;
  } else {
    // If no item was found, return an empty string (NULL)
    pStrItem[0] = 0;
    return false;
  }
}


int16_t gslc_ElemXListboxGetItemCnt(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return 0;

  return pListbox->nItemCnt;
}


// Create a Listbox element and add it to the GUI element list
// - Defines default styling for the element
// - Defines callback for redraw and touch
gslc_tsElemRef* gslc_ElemXListboxCreate(gslc_tsGui* pGui,int16_t nElemId,int16_t nPage,
  gslc_tsXListbox* pXData,gslc_tsRect rElem,int16_t nFontId,uint8_t* pBufItems,uint16_t nBufItemsMax,int16_t nItemDefault)
{
  if ((pGui == NULL) || (pXData == NULL)) {
    static const char GSLC_PMEM FUNCSTR[] = "ElemXListboxCreate";
    GSLC_DEBUG2_PRINT_CONST(ERRSTR_NULL,FUNCSTR);
    return NULL;
  }
  gslc_tsElem     sElem;
  gslc_tsElemRef* pElemRef = NULL;
  sElem = gslc_ElemCreate(pGui,nElemId,nPage,GSLC_TYPEX_LISTBOX,rElem,NULL,0,nFontId);
  sElem.nFeatures        &= ~GSLC_ELEM_FEA_FRAME_EN;
  sElem.nFeatures        |= GSLC_ELEM_FEA_FILL_EN;
  sElem.nFeatures        |= GSLC_ELEM_FEA_CLICK_EN;
  sElem.nFeatures        |= GSLC_ELEM_FEA_GLOW_EN;

  sElem.nGroup            = GSLC_GROUP_ID_NONE;
  pXData->pBufItems       = pBufItems;
  pXData->nBufItemsMax    = nBufItemsMax;
  pXData->nBufItemsPos    = 0;
  pXData->nItemCnt        = 0;
  pXData->nItemCurSel     = nItemDefault;
  pXData->nItemCurSelLast = XLISTBOX_SEL_NONE;
  pXData->nItemSavedSel   = XLISTBOX_SEL_NONE;
  pXData->nItemTop        = 0;
  pXData->pfuncXSel       = NULL;
  pXData->nCols           = 1;
  pXData->nRows           = XLISTBOX_SIZE_AUTO;     // Auto-calculated from content
  pXData->bNeedRecalc     = true;                   // Force auto-sizing
  pXData->nMarginW        = 5;
  pXData->nMarginH        = 5;
  pXData->bItemAutoSizeW  = true;                   // Force auto-sizing
  pXData->bItemAutoSizeH  = false;
  pXData->nItemW          = XLISTBOX_SIZE_AUTO;     // Auto-sized
  pXData->nItemH          = 30;
  pXData->nItemGap        = 2;
  pXData->colGap          = GSLC_COL_BLACK;
  pXData->nItemCurSelLast = XLISTBOX_SEL_NONE;
  sElem.pXData            = (void*)(pXData);
  // Specify the custom drawing callback
  sElem.pfuncXDraw        = &gslc_ElemXListboxDraw;
  // Specify the custom touch tracking callback
  sElem.pfuncXTouch       = &gslc_ElemXListboxTouch;

  sElem.colElemFill       = GSLC_COL_BLACK;
  sElem.colElemFillGlow   = GSLC_COL_BLACK;
  sElem.colElemFrame      = GSLC_COL_GRAY;
  sElem.colElemFrameGlow  = GSLC_COL_WHITE;

  // Set default text alignment:
  // - Vertical center, left justify
  sElem.eTxtAlign        = GSLC_ALIGN_MID_LEFT;

  if (nPage != GSLC_PAGE_NONE) {
    pElemRef = gslc_ElemAdd(pGui,nPage,&sElem,GSLC_ELEMREF_DEFAULT);
    return pElemRef;
#if (GSLC_FEATURE_COMPOUND)
  } else {
    // Save as temporary element
    pGui->sElemTmp = sElem;
    pGui->sElemRefTmp.pElem = &(pGui->sElemTmp);
    pGui->sElemRefTmp.eElemFlags = GSLC_ELEMREF_DEFAULT | GSLC_ELEMREF_REDRAW_FULL;
    return &(pGui->sElemRefTmp);
#endif
  }
  return NULL;
}


// Redraw the Listbox
// - Note that this redraw is for the entire element rect region
// - The Draw function parameters use void pointers to allow for
//   simpler callback function definition & scalability.
bool gslc_ElemXListboxDraw(void* pvGui,void* pvElemRef,gslc_teRedrawType eRedraw)
{
  // Typecast the parameters to match the GUI and element types
  gslc_tsGui*       pGui  = (gslc_tsGui*)(pvGui);
  gslc_tsElemRef*   pElemRef = (gslc_tsElemRef*)(pvElemRef);

  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  gslc_tsElem*      pElem = gslc_GetElemFromRef(pGui,pElemRef);

  //bool            bGlow     = (pElem->nFeatures & GSLC_ELEM_FEA_GLOW_EN) && gslc_ElemGetGlow(pGui,pElemRef);
  int8_t          nItemCurSel  = pListbox->nItemCurSel;


  gslc_tsRect rElemRect;
  if (pElem->nFeatures & GSLC_ELEM_FEA_FRAME_EN) {
    rElemRect = gslc_ExpandRect(pElem->rElem, -1, -1);
    if (eRedraw == GSLC_REDRAW_FULL) {
      gslc_DrawFrameRect(pGui, pElem->rElem, pElem->colElemFrame);
    }
  } else {
    rElemRect = pElem->rElem;
  }

  // If full redraw and gap is enabled:
  // - Clear background with gap color, as list items
  //   will be overdrawn in colBg color
  if (eRedraw == GSLC_REDRAW_FULL) {
    if (pListbox->nItemGap > 0) {
      gslc_DrawFillRect(pGui, rElemRect, pListbox->colGap);
    }
  }

  // Determine if we need to recalculate the item sizing
  if (pListbox->bNeedRecalc) {
    gslc_ElemXListboxRecalcSize(pListbox, rElemRect);
  }

  int16_t nX0, nY0;
  nX0 = rElemRect.x;
  nY0 = rElemRect.y;

  int8_t        nRows = pListbox->nRows;
  int8_t        nCols = pListbox->nCols;
  gslc_tsRect   rItemRect;
  int16_t       nItemBaseX, nItemBaseY;
  int16_t       nItemX, nItemY;
  int16_t       nItemW, nItemH;
  bool          bItemSel;
  gslc_tsColor  colFill, colTxt;

  // Error check
  if (nCols == 0) {
    return false;
  }

  // Determine top-left coordinate of list matrix
  nItemBaseX = nX0 + pListbox->nMarginW;
  nItemBaseY = nY0 + pListbox->nMarginH;
  char acStr[XLISTBOX_MAX_STR+1] = "";


  // Loop through the items in the list
  int8_t nItemTop = pListbox->nItemTop;
  int8_t nItemCnt = pListbox->nItemCnt;
  int8_t nItemInd;

  // Note that nItemTop is always pointing to an
  // item index at the start of a row

  // Determine the list indices to display in the visible window due to scrolling
  int8_t nDispIndMax = (nRows * nCols);

  for (int8_t nDispInd = 0; nDispInd < nDispIndMax; nDispInd++) {

    // Calculate the item index based on the display index
    nItemInd = nItemTop + nDispInd;

    // Did we go past the end of our list?
    if (nItemInd >= nItemCnt) {
      break;
    }

    // Fetch the list item
    bool bOk = gslc_ElemXListboxGetItem(pGui, pElemRef, nItemInd, acStr, XLISTBOX_MAX_STR);
    if (!bOk) {
      // TODO: Erorr handling
      break;
    }

    int8_t    nItemIndX, nItemIndY;
    int16_t   nItemOuterW, nItemOuterH;

    // Convert linear count into row & column
    nItemIndY = nDispInd / nCols; // Round down
    nItemIndX = nDispInd % nCols;

    // Calculate total spacing between items (including gap)
    nItemOuterW = pListbox->nItemW + pListbox->nItemGap;
    nItemOuterH = pListbox->nItemH + pListbox->nItemGap;

    // Determine top-left corner of each item
    nItemW = pListbox->nItemW;
    nItemH = pListbox->nItemH;
    nItemY = nItemBaseY + (nItemIndY * nItemOuterH);
    nItemX = nItemBaseX + (nItemIndX * nItemOuterW);

    // Create rect for item
    rItemRect = (gslc_tsRect) { nItemX, nItemY, nItemW, nItemH };

    // Is the item selected?
    bItemSel = (nItemInd == nItemCurSel) ? true : false;

    // Determine the color based on state
    colFill = (bItemSel) ? pElem->colElemFillGlow : pElem->colElemFill;
    colTxt = (bItemSel) ? pElem->colElemTextGlow : pElem->colElemText;

    bool bDoRedraw = false;
    if (eRedraw == GSLC_REDRAW_FULL) {
      bDoRedraw = true;
    } else if (eRedraw == GSLC_REDRAW_INC) {
      // Redraw both the old selected item (ie. to unselect it)
      // and the current selected item (ie. to select it)
      if (nItemInd == pListbox->nItemCurSelLast) {
        bDoRedraw = true;
      } else if (nItemInd == nItemCurSel) {
        bDoRedraw = true;
      }
    }

    // Draw the list item
    if (bDoRedraw) {
      gslc_DrawFillRect(pGui, rItemRect, colFill);

      // Set the text flags to indicate that the user has separately
      // allocated memory for the text strings.
      gslc_teTxtFlags eTxtFlags = GSLC_TXT_MEM_RAM | GSLC_TXT_ALLOC_EXT;

      // Draw the aligned text string (by default it is GSLC_ALIGN_MID_LEFT)
      gslc_DrawTxtBase(pGui, acStr, rItemRect, pElem->pTxtFont, eTxtFlags,
        pElem->eTxtAlign, colTxt, colFill, pElem->nTxtMarginX, pElem->nTxtMarginY);
    }

  }

  // Save the last selected item during redraw
  pListbox->nItemCurSelLast = nItemCurSel;

  // Clear the redraw flag
  gslc_ElemSetRedraw(pGui,pElemRef,GSLC_REDRAW_NONE);

  // Mark page as needing flip
  gslc_PageFlipSet(pGui,true);

  return true;
}

bool gslc_ElemXListboxTouch(void* pvGui, void* pvElemRef, gslc_teTouch eTouch, int16_t nRelX, int16_t nRelY)
{
  #if defined(DRV_TOUCH_NONE)
  return false;
  #else
  gslc_tsGui*           pGui = NULL;
  gslc_tsElemRef*       pElemRef = NULL;

  // Typecast the parameters to match the GUI
  pGui = (gslc_tsGui*)(pvGui);
  pElemRef = (gslc_tsElemRef*)(pvElemRef);

  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  //bool    bGlowingOld = gslc_ElemGetGlow(pGui, pElemRef);
  bool    bIndexed = false;

  int16_t nItemSavedSel = pListbox->nItemSavedSel;
  int16_t nItemCurSelOld = pListbox->nItemCurSel;
  int16_t nItemCurSel = XLISTBOX_SEL_NONE;
  bool bSelTrack = false;
  bool bSelSave = false;
  bool bSelInItem = true;

  switch (eTouch) {

  case GSLC_TOUCH_DOWN_IN:
    // Start glowing as must be over it
    gslc_ElemSetGlow(pGui, pElemRef, true);
    // User pressed inside elem: start selection
    bSelTrack = true;
    break;

  case GSLC_TOUCH_MOVE_IN:
    gslc_ElemSetGlow(pGui, pElemRef, true);
    // Track changes in selection
    bSelTrack = true;
    break;
  case GSLC_TOUCH_MOVE_OUT:
    gslc_ElemSetGlow(pGui, pElemRef, false);
    // User has dragged to outside elem: deselect
    bSelTrack = true;
    break;

  case GSLC_TOUCH_UP_IN:
    // End glow
    gslc_ElemSetGlow(pGui, pElemRef, false);
    // User released inside elem.
    // Save selection.
    // If selection is same as previous: toggle it
    bSelTrack = true;
    bSelSave = true;
    break;
  case GSLC_TOUCH_UP_OUT:
    // End glow
    gslc_ElemSetGlow(pGui, pElemRef, false);
    // User released outside elem: leave selection as-is
    bSelTrack = true;
    bSelSave = true; // Save SEL_NONE
    break;

  case GSLC_TOUCH_SET_REL:
  case GSLC_TOUCH_SET_ABS:
    bIndexed = true;
    gslc_ElemSetGlow(pGui,pElemRef,true);
    // Keyboard / pin control
    bSelTrack = true;
    bSelSave = true;
    break;

  default:
    return false;
    break;
  }

  uint8_t nCols = pListbox->nCols;
  uint8_t nRows = pListbox->nRows;

  // If we need to update the Listbox selection, calculate the value
  // and perform the update
  if (bSelTrack) {

    if (bIndexed) {
      // The selection is changed by direct control (eg. keyboard)
      // instead of touch coordinates


      // FIXME: Change the following to be either absolute or relative
      // value assignment instead of inc/dec. Then the user code can
      // define what the magnitude and direction should be.

      if (eTouch == GSLC_TOUCH_SET_REL) {
        // Overload the "nRelY" parameter as an increment value
        nItemCurSel = nItemCurSelOld + nRelY;
      }
      else if (eTouch == GSLC_TOUCH_SET_ABS) {
        // Overload the "nRelY" parameter as an absolute value
        nItemCurSel = nRelY;
      }
      gslc_ElemXListboxSetSel(pGui, pElemRef, nItemCurSel);
    }
    else {
      // Determine which item we are tracking
      int16_t nItemOuterW, nItemOuterH;
      int16_t nDispR, nDispC;
      int16_t nItemR, nItemC;

      // Get position relative to top-left list matrix cell
      nRelX -= pListbox->nMarginW;
      nRelY -= pListbox->nMarginH;

      // Determine spacing between matrix cells
      nItemOuterW = pListbox->nItemW + pListbox->nItemGap;
      nItemOuterH = pListbox->nItemH + pListbox->nItemGap;

      // Determine which matrix cell we are in
      nDispC = nRelX / nItemOuterW;
      nDispR = nRelY / nItemOuterH;

      if ((nRelX < 0) || (nRelY < 0)) {
        bSelInItem = false;
      }

      // Determine if the selection was inside the range of displayed items
      if ((nDispR < 0) || (nDispR >= nRows) || (nDispC < 0) || (nDispC >= nCols)) {
        bSelInItem = false;
      }
      if (bSelInItem) {

        // We have confirmed that the selected cell is
        // within the visible display range. Now translate
        // the display cell index to the absolute list cell index
        // by taking into account the scroll position.
        // - Note that nItemTop is always pointing to an
        //   item index at the start of a row
        nItemC = nDispC;
        nItemR = pListbox->nItemTop + nDispR;

        // Now we have identified the cell within the list matrix
        nItemCurSel = nItemR * nCols + nItemC;

        // Confirm we haven't selected out of range
        if (nItemCurSel >= pListbox->nItemCnt) {
          bSelInItem = false;
        }
      }

      if (bSelInItem) {
        // Now check for touch in gap region
        // - First find offset from top-left of matrix cell
        nRelX = nRelX % nItemOuterW;
        nRelY = nRelY % nItemOuterH;
        // If we are in the gap region, disable
        if (nRelX > pListbox->nItemW) {
          bSelInItem = false;
        }
        if (nRelY > pListbox->nItemH) {
          bSelInItem = false;
        }
      }

      // If we determined that the coordinate was not inside an
      // element, then clear current selection
      if (!bSelInItem) {
        nItemCurSel = XLISTBOX_SEL_NONE;
      }

      // If we have committed a touch press within an item
      // that was already selected, then toggle (deselect it)
      if ((bSelSave) && (nItemCurSel == nItemSavedSel)) {
        nItemCurSel = XLISTBOX_SEL_NONE;
      }

      bool bDoRedraw = false;

      if (nItemCurSel != nItemCurSelOld) {
        // Selection changed, so we will redraw
        bDoRedraw = true;
      }

      if (bDoRedraw) {
        // Update the selection
        gslc_ElemXListboxSetSel(pGui, pElemRef, nItemCurSel);

        // If any selection callback is defined, call it now
        if (pListbox->pfuncXSel != NULL) {
          (*pListbox->pfuncXSel)((void*)(pGui), (void*)(pElemRef), nItemCurSel);
        }
        // Redraw the element
        // - Note that ElemXListboxSetSel() above will also request redraw
        gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_INC);
      }

      // Update the saved selection
      if (bSelSave) {
        pListbox->nItemSavedSel = nItemCurSel;
      }

    } // bIndexed

  } // bSelTrack

  return true;
  
  #endif // DRV_TOUCH_NONE
}

int16_t gslc_ElemXListboxGetSel(gslc_tsGui* pGui,gslc_tsElemRef* pElemRef)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return XLISTBOX_SEL_NONE;

  return pListbox->nItemCurSel;
}

bool gslc_ElemXListboxSetSel(gslc_tsGui* pGui,gslc_tsElemRef* pElemRef, int16_t nItemCurSel)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  bool bOk = false;
  if (nItemCurSel == XLISTBOX_SEL_NONE) { bOk = true; }
  if ((nItemCurSel >= 0) && (nItemCurSel < pListbox->nItemCnt)) { bOk = true; }
  if (bOk) {
    pListbox->nItemCurSel = nItemCurSel;
    gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_INC);
  }
  return bOk;
}

bool gslc_ElemXListboxSetScrollPos(gslc_tsGui* pGui, gslc_tsElemRef* pElemRef, uint16_t nScrollPos)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return false;

  bool bOk = false;
  if ((nScrollPos >= 0) && (nScrollPos < pListbox->nItemCnt)) {
    bOk = true;
  }

  int16_t nCols = pListbox->nCols;
  int16_t nItemCnt = pListbox->nItemCnt;

  // Error handling: in case position out of bounds
  if (nScrollPos >= nItemCnt) {
    nScrollPos = (nItemCnt > 0) ? nItemCnt - 1 : 0;
  }

  // Adjust the top item index to the start of its row
  // - This is done because we may have multiple columns
  //   per row. This ensures nItemTop points to the list
  //   index at the start of a row.
  nScrollPos = (nScrollPos / nCols) * nCols;
  pListbox->nItemTop = nScrollPos;

  // Need to update all rows in display
  gslc_ElemSetRedraw(pGui, pElemRef, GSLC_REDRAW_FULL);
  return bOk;
}


// Assign the selection callback function for a Listbox
void gslc_ElemXListboxSetSelFunc(gslc_tsGui* pGui,gslc_tsElemRef* pElemRef,GSLC_CB_XLISTBOX_SEL funcCb)
{
  gslc_tsXListbox* pListbox = (gslc_tsXListbox*)gslc_GetXDataFromRef(pGui, pElemRef, GSLC_TYPEX_LISTBOX, __LINE__);
  if (!pListbox) return;

  pListbox->pfuncXSel = funcCb;
}

// ============================================================================
