/***********************************************************************
* flistview.cpp - Widget FListView and FListViewItem                   *
*                                                                      *
* This file is part of the FINAL CUT widget toolkit                    *
*                                                                      *
* Copyright 2017-2021 Markus Gans                                      *
*                                                                      *
* FINAL CUT is free software; you can redistribute it and/or modify    *
* it under the terms of the GNU Lesser General Public License as       *
* published by the Free Software Foundation; either version 3 of       *
* the License, or (at your option) any later version.                  *
*                                                                      *
* FINAL CUT is distributed in the hope that it will be useful, but     *
* WITHOUT ANY WARRANTY; without even the implied warranty of           *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        *
* GNU Lesser General Public License for more details.                  *
*                                                                      *
* You should have received a copy of the GNU Lesser General Public     *
* License along with this program.  If not, see                        *
* <http://www.gnu.org/licenses/>.                                      *
***********************************************************************/

#if defined(__CYGWIN__)
  #include <strings.h>  // need for strcasecmp
#endif

#include <limits>
#include <memory>
#include <unordered_map>
#include <utility>
#include <vector>

#include "final/emptyfstring.h"
#include "final/fapplication.h"
#include "final/fcolorpair.h"
#include "final/fevent.h"
#include "final/flistview.h"
#include "final/fstatusbar.h"
#include "final/fstring.h"
#include "final/ftermbuffer.h"
#include "final/fwidgetcolors.h"

namespace finalcut
{

// Function prototypes
uInt64 firstNumberFromString (const FString&);
bool sortAscendingByName (const FObject*, const FObject*);
bool sortDescendingByName (const FObject*, const FObject*);
bool sortAscendingByNumber (const FObject*, const FObject*);
bool sortDescendingByNumber (const FObject*, const FObject*);

// non-member functions
//----------------------------------------------------------------------
uInt64 firstNumberFromString (const FString& str)
{
  auto iter = str.begin();

  while ( iter != str.end() )
  {
    if ( wchar_t(*iter) >= L'0' && wchar_t(*iter) <= L'9' )
    {
      if ( iter != str.begin() && wchar_t(*(iter - 1)) == L'-' )
        --iter;

      break;
    }

    ++iter;
  }

  auto first_pos = iter;

  if ( first_pos == str.end() )
    return 0;

  while ( iter != str.end() )
  {
    if ( wchar_t(*iter) < L'0' || wchar_t(*iter) > L'9' )
      break;

    ++iter;
  }

  auto last_pos = iter;

  if ( last_pos == str.end() )
    return 0;

  uInt64 number;
  const auto pos = std::size_t(std::distance(str.begin(), first_pos)) + 1;
  const auto length = std::size_t(std::distance(first_pos, last_pos));
  const auto& num_str = str.mid(pos, length);

  try
  {
    number = uInt64(num_str.toLong());
  }
  catch (const std::invalid_argument&)
  {
    return 0;
  }
  catch (const std::underflow_error&)
  {
    return std::numeric_limits<uInt64>::min();
  }
  catch (const std::overflow_error&)
  {
    return std::numeric_limits<uInt64>::max();
  }

  return number;
}

//----------------------------------------------------------------------
bool sortAscendingByName (const FObject* lhs, const FObject* rhs)
{
  const auto& l_item = static_cast<const FListViewItem*>(lhs);
  const auto& r_item = static_cast<const FListViewItem*>(rhs);
  const int column = l_item->getSortColumn();
  const auto& l_string = l_item->getText(column);
  const auto& r_string = r_item->getText(column);

  // lhs < rhs
  return strcasecmp(l_string.c_str(), r_string.c_str()) < 0;
}

//----------------------------------------------------------------------
bool sortDescendingByName (const FObject* lhs, const FObject* rhs)
{
  const auto& l_item = static_cast<const FListViewItem*>(lhs);
  const auto& r_item = static_cast<const FListViewItem*>(rhs);
  const int column = l_item->getSortColumn();
  const auto& l_string = l_item->getText(column);
  const auto& r_string = r_item->getText(column);

  // lhs > rhs
  return strcasecmp(l_string.c_str(), r_string.c_str()) > 0;
}

//----------------------------------------------------------------------
bool sortAscendingByNumber (const FObject* lhs, const FObject* rhs)
{
  const auto& l_item = static_cast<const FListViewItem*>(lhs);
  const auto& r_item = static_cast<const FListViewItem*>(rhs);
  const int column = l_item->getSortColumn();
  const auto& l_number = firstNumberFromString(l_item->getText(column));
  const auto& r_number = firstNumberFromString(r_item->getText(column));

  // lhs < rhs
  return l_number < r_number;
}

//----------------------------------------------------------------------
bool sortDescendingByNumber (const FObject* lhs, const FObject* rhs)
{
  const auto& l_item = static_cast<const FListViewItem*>(lhs);
  const auto& r_item = static_cast<const FListViewItem*>(rhs);
  const int column = l_item->getSortColumn();
  const auto& l_number = firstNumberFromString(l_item->getText(column));
  const auto& r_number = firstNumberFromString(r_item->getText(column));

  // lhs > rhs
  return l_number > r_number;
}


//----------------------------------------------------------------------
// class FListViewItem
//----------------------------------------------------------------------

// constructor and destructor
//----------------------------------------------------------------------
FListViewItem::FListViewItem (const FListViewItem& item)
  : FObject{item.getParent()}
  , column_list{item.column_list}
  , data_pointer{item.data_pointer}
{
  auto parent = getParent();

  if ( ! parent )
    return;

  if ( parent->isInstanceOf("FListView") )
  {
    static_cast<FListView*>(parent)->insert (this);
  }
  else if ( parent->isInstanceOf("FListViewItem") )
  {
    static_cast<FListViewItem*>(parent)->insert (this);
  }
}

//----------------------------------------------------------------------
FListViewItem::FListViewItem (iterator parent_iter)
  : FObject{(*parent_iter)->getParent()}
{
  insert (this, parent_iter);
}

//----------------------------------------------------------------------
FListViewItem::~FListViewItem()  // destructor
{
  // Remove from parent itemlist

  auto parent = getParent();

  if ( ! parent )
    return;

  if ( parent->isInstanceOf("FListView") )
  {
    static_cast<FListView*>(parent)->remove (this);
  }
  else if ( parent->isInstanceOf("FListViewItem") )
  {
    static_cast<FListViewItem*>(parent)->remove (this);
  }
}


// public methods of FListViewItem
//----------------------------------------------------------------------
int FListViewItem::getSortColumn() const
{
  if ( ! *root )
    return -1;

  const auto& root_obj = static_cast<FListView*>(*root);
  return root_obj->getSortColumn();
}

//----------------------------------------------------------------------
FString FListViewItem::getText (int column) const
{
  if ( column < 1
    || column_list.empty()
    || column > int(column_list.size()) )
    return fc::emptyFString::get();

  // Convert column position to address offset (index)
  const auto index = std::size_t(column - 1);
  return column_list[index];
}

//----------------------------------------------------------------------
uInt FListViewItem::getDepth() const
{
  const auto& parent = getParent();

  if ( parent && parent->isInstanceOf("FListViewItem") )
  {
    const auto& parent_item = static_cast<FListViewItem*>(parent);
    return parent_item->getDepth() + 1;
  }

  return 0;
}

//----------------------------------------------------------------------
void FListViewItem::setText (int column, const FString& text)
{
  if ( column < 1
    || column_list.empty()
    || column > int(column_list.size()) )
    return;

  // Convert column position to address offset (index)
  const auto index = std::size_t(column - 1);
  auto parent = getParent();

  if ( parent && parent->isInstanceOf("FListView") )
  {
    auto listview = static_cast<FListView*>(parent);

    if ( ! listview->header[index].fixed_width )
    {
      const auto column_width = int(getColumnWidth(text));

      if ( column_width > listview->header[index].width )
        listview->header[index].width = column_width;
    }
  }

  column_list[index] = text;
}

//----------------------------------------------------------------------
FObject::iterator FListViewItem::insert (FListViewItem* child)
{
  // Add a FListViewItem as child element
  if ( ! child )
    return FListView::getNullIterator();

  return appendItem(child);
}

//----------------------------------------------------------------------
FObject::iterator FListViewItem::insert ( FListViewItem* child
                                        , iterator parent_iter ) const
{
  if ( parent_iter == FListView::getNullIterator() )
    return FListView::getNullIterator();

  if ( *parent_iter )
  {
    if ( (*parent_iter)->isInstanceOf("FListView") )
    {
      // Add FListViewItem to a FListView parent
      auto parent = static_cast<FListView*>(*parent_iter);
      return parent->insert (child);
    }
    else if ( (*parent_iter)->isInstanceOf("FListViewItem") )
    {
      // Add FListViewItem to a FListViewItem parent
      auto parent = static_cast<FListViewItem*>(*parent_iter);
      return parent->insert (child);
    }
  }

  return FListView::getNullIterator();
}

//----------------------------------------------------------------------
void FListViewItem::remove (FListViewItem* item) const
{
  if ( item == nullptr || item == *FListView::getNullIterator() )
    return;

  auto parent = item->getParent();

  // Search for a FListView parent in my object tree
  while ( parent && ! parent->isInstanceOf("FListView") )
  {
    parent = parent->getParent();
  }

  if ( parent == nullptr )
    return;

  if ( parent->isInstanceOf("FListView") )
  {
    auto listview = static_cast<FListView*>(parent);
    listview->remove(item);
  }
  else
  {
    parent = item->getParent();
    parent->delChild(item);
    auto parent_item = static_cast<FListViewItem*>(parent);
    parent_item->visible_lines--;

    if ( ! parent_item->hasChildren() )
    {
      parent_item->expandable = false;
      parent_item->is_expand = false;
    }
  }
}

//----------------------------------------------------------------------
void FListViewItem::expand()
{
  if ( isExpand() || ! hasChildren() )
    return;

  resetVisibleLineCounter();
  is_expand = true;
}

//----------------------------------------------------------------------
void FListViewItem::collapse()
{
  if ( ! isExpand() )
    return;

  resetVisibleLineCounter();
  is_expand = false;
}

// private methods of FListView
//----------------------------------------------------------------------
template <typename Compare>
void FListViewItem::sort (Compare cmp)
{
  if ( ! isExpandable() )
    return;

  // Sort the top level
  FObject::FObjectList& children = getChildren();

  if ( ! children.empty() )
    children.sort(cmp);

  // Sort the sublevels
  for (auto&& item : children)
    static_cast<FListViewItem*>(item)->sort(cmp);
}

//----------------------------------------------------------------------
FObject::iterator FListViewItem::appendItem (FListViewItem* child)
{
  expandable = true;
  resetVisibleLineCounter();
  child->root = root;
  addChild (child);
  // Return iterator to child/last element
  return --FObject::end();
}

//----------------------------------------------------------------------
void FListViewItem::replaceControlCodes()
{
  // Replace the control codes characters
  auto iter = column_list.begin();

  while ( iter != column_list.end() )
  {
    *iter = iter->replaceControlCodes();
    ++iter;
  }
}

//----------------------------------------------------------------------
std::size_t FListViewItem::getVisibleLines()
{
  if ( visible_lines > 1 )
    return visible_lines;

  visible_lines = 1;

  if ( ! isExpand() || ! hasChildren() )
    return visible_lines;

  auto iter = FObject::begin();

  while ( iter != FObject::end() )
  {
    const auto& child = static_cast<FListViewItem*>(*iter);
    visible_lines += child->getVisibleLines();
    ++iter;
  }

  return visible_lines;
}

//----------------------------------------------------------------------
void FListViewItem::setCheckable (bool enable)
{
  checkable = enable;

  if ( *root )
  {
    auto root_obj = static_cast<FListView*>(*root);

    if ( ! root_obj->hasCheckableItems() && isCheckable() )
      root_obj->has_checkable_items = true;
  }
}

//----------------------------------------------------------------------
void FListViewItem::resetVisibleLineCounter()
{
  visible_lines = 0;
  auto parent = getParent();

  if ( parent && parent->isInstanceOf("FListViewItem") )
  {
    auto parent_item = static_cast<FListViewItem*>(parent);
    return parent_item->resetVisibleLineCounter();
  }
}


//----------------------------------------------------------------------
// class FListViewIterator
//----------------------------------------------------------------------

// constructor and destructor
//----------------------------------------------------------------------
FListViewIterator::FListViewIterator (Iterator iter)
  : node{iter}
{ }

// FListViewIterator operators
//----------------------------------------------------------------------
FListViewIterator& FListViewIterator::operator ++ ()  // prefix
{
  nextElement(node);
  return *this;
}

//----------------------------------------------------------------------
FListViewIterator FListViewIterator::operator ++ (int)  // postfix
{
  FListViewIterator tmp = *this;
  ++(*this);
  return tmp;
}

//----------------------------------------------------------------------
FListViewIterator& FListViewIterator::operator -- ()  // prefix
{
  prevElement(node);
  return *this;
}

//----------------------------------------------------------------------
FListViewIterator FListViewIterator::operator -- (int)  // postfix
{
  FListViewIterator tmp = *this;
  --(*this);
  return tmp;
}

//----------------------------------------------------------------------
FListViewIterator& FListViewIterator::operator += (int n)
{
  for (int i = n; i > 0 ; i--)
    nextElement(node);

  return *this;
}

//----------------------------------------------------------------------
FListViewIterator& FListViewIterator::operator -= (int n)
{
  for (int i = n; i > 0 ; i--)
    prevElement(node);

  return *this;
}

// private methods of FListViewIterator
//----------------------------------------------------------------------
void FListViewIterator::nextElement (Iterator& iter)
{
  const auto& item = static_cast<FListViewItem*>(*iter);

  if ( item->isExpandable() && item->isExpand() )
  {
    iter_path.push(iter);
    iter = item->begin();
    position++;
  }
  else
  {
    position++;
    bool forward{};

    do
    {
      forward = false;  // Reset forward
      ++iter;

      if ( ! iter_path.empty() )
      {
        const auto& parent_iter = iter_path.top();

        if ( iter == (*parent_iter)->end() )
        {
          iter = parent_iter;
          iter_path.pop();
          forward = true;
        }
      }
    }
    while ( forward );
  }
}

//----------------------------------------------------------------------
void FListViewIterator::prevElement (Iterator& iter)
{
  auto start_iter = iter;

  if ( ! iter_path.empty() )
  {
    const auto& parent_iter = iter_path.top();

    if ( start_iter == (*parent_iter)->begin() )  // First sub-item
    {
      iter = parent_iter;
      position--;
      iter_path.pop();
      return;
    }
  }

  --iter;
  auto item = static_cast<FListViewItem*>(*iter);

  if ( iter == start_iter )  // No changes
    return;
  else
    position--;

  while ( item->isExpandable() && item->isExpand() )
  {
    iter_path.push(iter);
    iter = item->end();
    --iter;
    item = static_cast<FListViewItem*>(*iter);
  }
}

//----------------------------------------------------------------------
void FListViewIterator::parentElement()
{
  if ( iter_path.empty() )
    return;

  const auto& parent_iter = iter_path.top();

  while ( node != parent_iter )
    prevElement(node);
}


//----------------------------------------------------------------------
// class FListView
//----------------------------------------------------------------------

// constructor and destructor
//----------------------------------------------------------------------
FListView::FListView (FWidget* parent)
  : FWidget{parent}
{
  init();
}

//----------------------------------------------------------------------
FListView::~FListView()  // destructor
{
  delOwnTimers();
}

// public methods of FListView
//----------------------------------------------------------------------
std::size_t FListView::getCount() const
{
  int n{0};

  for (auto&& item : itemlist)
  {
    const auto& listitem = static_cast<FListViewItem*>(item);
    n += listitem->getVisibleLines();
  }

  return std::size_t(n);
}

//----------------------------------------------------------------------
Align FListView::getColumnAlignment (int column) const
{
  // Get the alignment for a column

  if ( column < 1 || header.empty() || column > int(header.size()) )
    return Align::Left;

  // Convert column position to address offset (index)
  const auto index = std::size_t(column - 1);
  return header[index].alignment;
}

//----------------------------------------------------------------------
FString FListView::getColumnText (int column) const
{
  // Get the text of column

  if ( column < 1 || header.empty() || column > int(header.size()) )
    return fc::emptyFString::get();

  // Convert column position to address offset (index)
  const auto index = std::size_t(column - 1);
  return header[index].name;
}

//----------------------------------------------------------------------
SortType FListView::getColumnSortType (int column) const
{
  SortType s_type;
  const auto col = std::size_t(column);

  try
  {
    s_type = sort_type.at(col);
  }
  catch (const std::out_of_range&)
  {
    s_type = SortType::Unknown;
  }

  return s_type;
}

//----------------------------------------------------------------------
void FListView::setSize (const FSize& size, bool adjust)
{
  FWidget::setSize (size, adjust);
  changeOnResize();
}

//----------------------------------------------------------------------
void FListView::setGeometry ( const FPoint& pos, const FSize& size
                            , bool adjust)
{
  FWidget::setGeometry (pos, size, adjust);
  changeOnResize();
}

//----------------------------------------------------------------------
void FListView::setColumnAlignment (int column, Align align)
{
  // Set the alignment for a column

  if ( column < 1 || header.empty() || column > int(header.size()) )
    return;

  // Convert column position to address offset (index)
  const auto index = std::size_t(column - 1);
  header[index].alignment = align;
}

//----------------------------------------------------------------------
void FListView::setColumnText (int column, const FString& label)
{
  // Set the text for a column

  if ( column < 1 || header.empty() || column > int(header.size()) )
    return;

  // Convert column position to address offset (index)
  auto index = std::size_t(column - 1);

  if ( ! header[index].fixed_width )
  {
    const auto column_width = int(getColumnWidth(label));

    if ( column_width > header[index].width )
      header[index].width = column_width;
  }

  header[index].name = label;
}

//----------------------------------------------------------------------
void FListView::setColumnSortType (int column, SortType type)
{
  // Sets the sort type by which the list is to be sorted

  if ( column < 1 || header.empty() || column > int(header.size()) )
    return;

  const std::size_t size = std::size_t(column) + 1;

  if ( sort_type.empty() || sort_type.size() < size )
    sort_type.resize(size);

  sort_type[uInt(column)] = type;
}

//----------------------------------------------------------------------
void FListView::setColumnSort (int column, SortOrder order)
{
  // Sets the column to sort by + the sorting order

  if ( column < 1 || header.empty() || column > int(header.size()) )
    column = -1;

  sort_column = column;
  sort_order = order;
}

//----------------------------------------------------------------------
int FListView::addColumn (const FString& label, int width)
{
  Header new_column{};
  new_column.name = label;
  new_column.width = width;

  if ( new_column.width == USE_MAX_SIZE )
    new_column.width = int(getColumnWidth(label));
  else
    new_column.fixed_width = true;

  header.push_back (new_column);
  return int(std::distance(header.begin(), header.end()));
}

//----------------------------------------------------------------------
void FListView::hide()
{
  FWidget::hide();
  hideArea (getSize());
}

//----------------------------------------------------------------------
FObject::iterator FListView::insert ( FListViewItem* item
                                    , iterator parent_iter )
{
  iterator item_iter;

  if ( parent_iter == getNullIterator() )
    return getNullIterator();

  beforeInsertion(item);  // preprocessing

  if ( parent_iter == root )
  {
    item_iter = appendItem (item);
  }
  else if ( *parent_iter )
  {
    if ( (*parent_iter)->isInstanceOf("FListView") )
    {
      // Add FListViewItem to a FListView parent
      auto parent = static_cast<FListView*>(*parent_iter);
      item_iter = parent->appendItem (item);
    }
    else if ( (*parent_iter)->isInstanceOf("FListViewItem") )
    {
      // Add FListViewItem to a FListViewItem parent
      auto parent = static_cast<FListViewItem*>(*parent_iter);
      item_iter = parent->appendItem (item);
    }
    else
      item_iter = getNullIterator();
  }
  else
    item_iter = getNullIterator();

  afterInsertion();  // post-processing
  return item_iter;
}

//----------------------------------------------------------------------
void FListView::remove (FListViewItem* item)
{
  if ( item == nullptr || itemlist.empty() )
    return;

  auto parent = item->getParent();
  const auto& current_item = static_cast<FListViewItem*>(*current_iter);
  const auto& first_item = itemlist.front();
  auto end_iter = static_cast<FListViewIterator>(itemlist.end());
  const auto& last_item = *(--end_iter);
  const bool is_current_line( item == current_item );
  const bool is_first_line( item == first_item );
  const bool is_last_line( item == last_item );

  if ( is_current_line )
  {
    if ( is_last_line || current_item == itemlist.back() )
      stepBackward();
    else
    {
      collapseSubtree();
      stepForward();
    }
  }

  if ( is_first_line )
    ++first_visible_line;

  if ( parent )
  {
    if ( this == parent )
    {
      itemlist.remove(item);
      delChild(item);
      current_iter.getPosition()--;
    }
    else
    {
      parent->delChild(item);
      auto parent_item = static_cast<FListViewItem*>(parent);
      parent_item->visible_lines--;
      current_iter.getPosition()--;

      if ( ! parent_item->hasChildren() )
      {
        parent_item->expandable = false;
        parent_item->is_expand = false;
      }
    }
  }

  const std::size_t element_count = getCount();
  recalculateVerticalBar (element_count);

  if ( itemlist.empty() )
  {
    current_iter = getNullIterator();
    first_visible_line = getNullIterator();
    last_visible_line = getNullIterator();
    clearList();
  }
  else
  {
    drawList();
    drawBorder();
    drawHeadlines();
    drawScrollbars();
  }
}

//----------------------------------------------------------------------
void FListView::clear()
{
  itemlist.clear();
  current_iter = getNullIterator();
  first_visible_line = getNullIterator();
  last_visible_line = getNullIterator();
  recalculateVerticalBar (0);
  first_line_position_before = -1;
  xoffset = 0;
  vbar->setMinimum(0);
  vbar->setValue(0);
  vbar->hide();
  clearList();
}

//----------------------------------------------------------------------
void FListView::sort()
{
  // Sorts the list view according to the specified setting

  if ( sort_column < 1 && sort_column > int(header.size()) )
    return;

  SortType column_sort_type = getColumnSortType(sort_column);
  assert ( column_sort_type == SortType::Name
        || column_sort_type == SortType::Number
        || column_sort_type == SortType::UserDefined
        || column_sort_type == SortType::Unknown );

  switch ( column_sort_type )
  {
    case SortType::Unknown:
    case SortType::Name:
      if ( sort_order == SortOrder::Ascending )
      {
        sort (sortAscendingByName);
      }
      else if ( sort_order == SortOrder::Descending )
      {
        sort (sortDescendingByName);
      }
      break;

    case SortType::Number:
      if ( sort_order == SortOrder::Ascending )
      {
        sort (sortAscendingByNumber);
      }
      else if ( sort_order == SortOrder::Descending )
      {
        sort (sortDescendingByNumber);
      }
      break;

    case SortType::UserDefined:
      if ( sort_order == SortOrder::Ascending && user_defined_ascending )
      {
        sort (user_defined_ascending);
      }
      else if ( sort_order == SortOrder::Descending && user_defined_descending )
      {
        sort (user_defined_descending);
      }
      break;
  }

  current_iter = itemlist.begin();
  first_visible_line = itemlist.begin();
}

//----------------------------------------------------------------------
void FListView::onKeyPress (FKeyEvent* ev)
{
  const int position_before = current_iter.getPosition();
  const int xoffset_before = xoffset;
  first_line_position_before = first_visible_line.getPosition();
  clicked_expander_pos.setPoint(-1, -1);
  processKeyAction(ev);  // Process the keystrokes

  if ( position_before != current_iter.getPosition() )
    processChanged();

  if ( ev->isAccepted() )
  {
    const bool draw_vbar( first_line_position_before
                       != first_visible_line.getPosition() );
    const bool draw_hbar(xoffset_before != xoffset);
    updateDrawing (draw_vbar, draw_hbar);
  }
}

//----------------------------------------------------------------------
void FListView::onMouseDown (FMouseEvent* ev)
{
  if ( ev->getButton() != MouseButton::Left )
  {
    clicked_expander_pos.setPoint(-1, -1);
    return;
  }

  setWidgetFocus(this);
  const int mouse_x = ev->getX();
  const int mouse_y = ev->getY();
  first_line_position_before = first_visible_line.getPosition();

  if ( mouse_x > 1 && mouse_x < int(getWidth()) )
  {
    if ( mouse_y == 1 )  // Header
    {
      clicked_header_pos = ev->getPos();
    }
    else if ( mouse_y > 1 && mouse_y < int(getHeight()) )  // List
    {
      if ( itemlist.empty() )
        return;

      int indent = 0;
      const int new_pos = first_visible_line.getPosition() + mouse_y - 2;

      if ( new_pos < int(getCount()) )
        setRelativePosition (mouse_y - 2);

      const auto& item = getCurrentItem();

      if ( tree_view )
      {
        indent = int(item->getDepth() << 1);  // indent = 2 * depth

        if ( item->isExpandable() && mouse_x - 2 == indent - xoffset )
          clicked_expander_pos = ev->getPos();
      }

      if ( hasCheckableItems() )
      {
        if ( tree_view )
          indent++;  // Plus one space

        if ( mouse_x >= 3 + indent - xoffset
          && mouse_x <= 5 + indent - xoffset
          && item->isCheckable() )
        {
          clicked_checkbox_item = item;
        }
      }

      if ( isShown() )
        drawList();

      vbar->setValue (first_visible_line.getPosition());

      if ( first_line_position_before != first_visible_line.getPosition() )
        vbar->drawBar();

      forceTerminalUpdate();
    }
  }
}

//----------------------------------------------------------------------
void FListView::onMouseUp (FMouseEvent* ev)
{
  if ( drag_scroll != DragScrollMode::None )
    stopDragScroll();

  if ( ev->getButton() == MouseButton::Left )
  {
    const int mouse_x = ev->getX();
    const int mouse_y = ev->getY();

    if ( mouse_x > 1 && mouse_x < int(getWidth()) )
    {
      if ( mouse_y == 1 && clicked_header_pos == ev->getPos() )  // Header
      {
        mouseHeaderClicked();
      }
      else if ( mouse_y > 1 && mouse_y < int(getHeight()) )  // List
      {
        if ( itemlist.empty() )
          return;

        int indent{0};
        const auto& item = getCurrentItem();

        if ( tree_view )
        {
          indent = int(item->getDepth() << 1);  // indent = 2 * depth

          if ( item->isExpandable()
            && clicked_expander_pos == ev->getPos() )
          {
            if ( item->isExpand() )
              item->collapse();
            else
              item->expand();

            adjustScrollbars (getCount());

            if ( isShown() )
              draw();
          }
        }

        if ( hasCheckableItems() )
        {
          if ( tree_view )
            indent++;  // Plus one space

          if ( mouse_x >= 3 + indent - xoffset
            && mouse_x <= 5 + indent - xoffset
            && clicked_checkbox_item == item )
          {
            item->setChecked(! item->isChecked());

            if ( isShown() )
              draw();
          }
        }

        processChanged();
      }
    }
  }

  clicked_expander_pos.setPoint(-1, -1);
  clicked_header_pos.setPoint(-1, -1);
  clicked_checkbox_item = nullptr;
}

//----------------------------------------------------------------------
void FListView::onMouseMove (FMouseEvent* ev)
{
  if ( ev->getButton() != MouseButton::Left )
  {
    clicked_expander_pos.setPoint(-1, -1);
    return;
  }

  const int mouse_x = ev->getX();
  const int mouse_y = ev->getY();
  first_line_position_before = first_visible_line.getPosition();

  if ( mouse_x > 1 && mouse_x < int(getWidth())
    && mouse_y > 1 && mouse_y < int(getHeight()) )
  {
    const int new_pos = first_visible_line.getPosition() + mouse_y - 2;

    if ( new_pos < int(getCount()) )
      setRelativePosition (mouse_y - 2);

    if ( isShown() )
      drawList();

    vbar->setValue (first_visible_line.getPosition());

    if ( first_line_position_before != first_visible_line.getPosition() )
      vbar->drawBar();

    forceTerminalUpdate();
  }

  // auto-scrolling when dragging mouse outside the widget
  if ( mouse_y < 2 )
    dragUp (ev->getButton());
  else if ( mouse_y >= int(getHeight()) )
    dragDown (ev->getButton());
  else
    stopDragScroll();
}

//----------------------------------------------------------------------
void FListView::onMouseDoubleClick (FMouseEvent* ev)
{
  if ( ev->getButton() != MouseButton::Left )
    return;

  const int mouse_x = ev->getX();
  const int mouse_y = ev->getY();

  if ( mouse_x > 1 && mouse_x < int(getWidth())
    && mouse_y > 1 && mouse_y < int(getHeight()) )
  {
    if ( first_visible_line.getPosition() + mouse_y - 1 > int(getCount()) )
      return;

    if ( itemlist.empty() )
      return;

    auto item = getCurrentItem();

    if ( tree_view && item->isExpandable() )
    {
      if ( item->isExpand() )
        item->collapse();
      else
        item->expand();

      adjustScrollbars (getCount());  // after expand or collapse

      if ( isShown() )
        draw();
    }

    processClick();
  }

  clicked_expander_pos.setPoint(-1, -1);
}

//----------------------------------------------------------------------
void FListView::onTimer (FTimerEvent*)
{
  const int position_before = current_iter.getPosition();
  first_line_position_before = first_visible_line.getPosition();

  if ( ( drag_scroll == DragScrollMode::Upward
      || drag_scroll == DragScrollMode::SelectUpward )
      && ! dragScrollUp(position_before) )
  {
    return;
  }
  else if ( ( drag_scroll == DragScrollMode::Downward
           || drag_scroll == DragScrollMode::SelectDownward )
           && ! dragScrollDown(position_before) )
  {
    return;
  }

  if ( isShown() )
    drawList();

  vbar->setValue (first_visible_line.getPosition());

  if ( first_line_position_before != first_visible_line.getPosition() )
    vbar->drawBar();

  forceTerminalUpdate();
}

//----------------------------------------------------------------------
void FListView::onWheel (FWheelEvent* ev)
{
  const int position_before = current_iter.getPosition();
  static constexpr int wheel_distance = 4;
  first_line_position_before = first_visible_line.getPosition();

  if ( drag_scroll != DragScrollMode::None )
    stopDragScroll();

  if ( ev->getWheel() == MouseWheel::Up )
    wheelUp (wheel_distance);
  else if ( ev->getWheel() == MouseWheel::Down )
    wheelDown (wheel_distance);

  if ( position_before != current_iter.getPosition() )
    processChanged();

  if ( isShown() )
    drawList();

  vbar->setValue (first_visible_line.getPosition());

  if ( first_line_position_before != first_visible_line.getPosition() )
    vbar->drawBar();

  forceTerminalUpdate();
}

//----------------------------------------------------------------------
void FListView::onFocusIn (FFocusEvent*)
{
  if ( getStatusBar() )
    getStatusBar()->drawMessage();
}

//----------------------------------------------------------------------
void FListView::onFocusOut (FFocusEvent*)
{
  if ( getStatusBar() )
  {
    getStatusBar()->clearMessage();
    getStatusBar()->drawMessage();
  }

  delOwnTimers();
}


// protected methods of FListView
//----------------------------------------------------------------------
void FListView::adjustViewport (const int element_count)
{
  const auto height = int(getClientHeight());

  if ( height <= 0 || element_count == 0 )
    return;

  if ( element_count < height )
  {
    first_visible_line = itemlist.begin();
    last_visible_line = first_visible_line;
    last_visible_line += element_count - 1;
  }

  if ( first_visible_line.getPosition() > element_count - height  )
  {
    const int difference = first_visible_line.getPosition()
                         - (element_count - height);

    if ( first_visible_line.getPosition() >= difference )
    {
      first_visible_line -= difference;
      last_visible_line -= difference;
    }
  }

  const int after_last_visible_line = first_visible_line.getPosition()
                                    + height;

  if ( last_visible_line.getPosition() >= after_last_visible_line )
  {
    last_visible_line = first_visible_line;
    last_visible_line += height - 1;
  }

  if ( current_iter.getPosition() > last_visible_line.getPosition() )
    current_iter = last_visible_line;
}

//----------------------------------------------------------------------
void FListView::adjustScrollbars (const std::size_t element_count) const
{
  const std::size_t width = getClientWidth();
  const std::size_t height = getClientHeight();
  const int vmax = ( element_count > height )
                   ? int(element_count - height)
                   : 0;
  vbar->setMaximum (vmax);
  vbar->setPageSize (int(element_count), int(height));
  vbar->setX (int(getWidth()));
  vbar->setHeight (height, false);
  vbar->resize();

  const int hmax = ( max_line_width > width )
                   ? int(max_line_width - width)
                   : 0;
  hbar->setMaximum (hmax);
  hbar->setPageSize (int(max_line_width), int(width));
  hbar->setY (int(getHeight()));
  hbar->setWidth (width, false);
  hbar->resize();

  if ( isHorizontallyScrollable() )
    hbar->show();
  else
    hbar->hide();

  if ( isVerticallyScrollable() )
    vbar->show();
  else
    vbar->hide();
}

//----------------------------------------------------------------------
void FListView::adjustSize()
{
  FWidget::adjustSize();
  const std::size_t element_count = getCount();
  adjustViewport (int(element_count));
  adjustScrollbars (element_count);
}


// private methods of FListView
//----------------------------------------------------------------------
FObject::iterator& FListView::getNullIterator()
{
  static iterator null_iter;  // Saves the global null iterator
  return null_iter;
}

//----------------------------------------------------------------------
void FListView::setNullIterator (const iterator& null_iter)
{
  getNullIterator() = null_iter;
}

//----------------------------------------------------------------------
void FListView::init()
{
  initScrollbar (vbar, Orientation::Vertical, this, &FListView::cb_vbarChange);
  initScrollbar (hbar, Orientation::Horizontal, this, &FListView::cb_hbarChange);
  selflist.push_back(this);
  root = selflist.begin();
  getNullIterator() = selflist.end();
  setGeometry (FPoint{1, 1}, FSize{5, 4}, false);  // initialize geometry values
  nf_offset = FTerm::isNewFont() ? 1 : 0;
  setTopPadding(1);
  setLeftPadding(1);
  setBottomPadding(1);
  setRightPadding(1);
  mapKeyFunctions();
}

//----------------------------------------------------------------------
inline void FListView::mapKeyFunctions()
{
  key_map[FKey::Return]     = [this] { processClick(); };
  key_map[FKey::Enter]      = [this] { processClick(); };
  key_map[FKey::Space]      = [this] { toggleCheckbox(); };
  key_map[FKey::Up]         = [this] { stepBackward(); };
  key_map[FKey::Down]       = [this] { stepForward(); };
  key_map[FKey::Left]       = [this] { collapseAndScrollLeft(); };
  key_map[FKey::Right]      = [this] { expandAndScrollRight(); };
  key_map[FKey::Page_up]    = [this] { stepBackward(int(getClientHeight()) - 1); };
  key_map[FKey::Page_down]  = [this] { stepForward(int(getClientHeight()) - 1); };
  key_map[FKey::Home]       = [this] { firstPos(); };
  key_map[FKey::End]        = [this] { lastPos(); };
  key_map_result[FKey('+')] = [this] { return expandSubtree(); };
  key_map_result[FKey('-')] = [this] { return collapseSubtree(); };
}

//----------------------------------------------------------------------
void FListView::processKeyAction (FKeyEvent* ev)
{
  const auto idx = ev->key();
  const auto& entry = key_map[idx];

  if ( entry )
  {
    entry();
    ev->accept();
  }
  else
  {
    const auto& entry_result = key_map_result[idx];

    if ( entry_result )
    {
      if ( entry_result() )
        ev->accept();
    }
    else
    {
      ev->ignore();
    }
  }
}

//----------------------------------------------------------------------
template <typename Compare>
void FListView::sort (Compare cmp)
{
  // Sort the top level
  itemlist.sort(cmp);

  // Sort the sublevels
  for (auto&& item : itemlist)
    static_cast<FListViewItem*>(item)->sort(cmp);
}

//----------------------------------------------------------------------
std::size_t FListView::getAlignOffset ( const Align align
                                      , const std::size_t column_width
                                      , const std::size_t width ) const
{
  if ( align == Align::Center )
  {
    if ( column_width < width )
      return (width - column_width) / 2;
  }
  else if ( align == Align::Right && column_width < width )
  {
    return width - column_width;
  }

  return 0;
}

//----------------------------------------------------------------------
FObject::iterator FListView::getListEnd (const FListViewItem* item)
{
  auto parent = item->getParent();

  if ( ! parent )
    return getNullIterator();

  if ( this == parent )
    return itemlist.end();
  else if ( parent->isInstanceOf("FListViewItem") )
    return static_cast<FListViewItem*>(parent)->end();
  else
    return getNullIterator();
}

//----------------------------------------------------------------------
void FListView::draw()
{
  if ( current_iter.getPosition() < 1 )
    current_iter = itemlist.begin();

  useParentWidgetColor();

  if ( FTerm::isMonochron() )
    setReverse(true);

  drawBorder();

  if ( FTerm::isNewFont() && ! vbar->isShown() )
  {
    setColor();

    for (auto y{2}; y < int(getHeight()); y++)
    {
      print() << FPoint{int(getWidth()) - 1, y}
              << ' ';  // clear right side of the scrollbar
    }
  }

  drawHeadlines();

  if ( FTerm::isMonochron() )
    setReverse(false);

  drawScrollbars();
  drawList();

  if ( getFlags().focus && getStatusBar() )
  {
    const auto& msg = getStatusbarMessage();
    const auto& curMsg = getStatusBar()->getMessage();

    if ( curMsg != msg )
    {
      getStatusBar()->setMessage(msg);
      getStatusBar()->drawMessage();
    }
  }
}

//----------------------------------------------------------------------
void FListView::drawBorder()
{
  const FRect box(FPoint{1, 1}, getSize());
  finalcut::drawListBorder (this, box);
}

//----------------------------------------------------------------------
void FListView::drawScrollbars() const
{
  if ( ! hbar->isShown() && isHorizontallyScrollable() )
    hbar->show();
  else
    hbar->redraw();

  if ( ! vbar->isShown() && isVerticallyScrollable() )
    vbar->show();
  else
    vbar->redraw();
}

//----------------------------------------------------------------------
void FListView::drawHeadlines()
{
  if ( header.empty()
    || getHeight() <= 2
    || getWidth() <= 4
    || max_line_width < 1 )
    return;

  HeaderItems::const_iterator iter = header.begin();
  headerline.clear();

  if ( hasCheckableItems() )
    drawHeaderBorder(4);  // Draw into FTermBuffer object

  while ( iter != header.end() )
  {
    if ( ! iter->name.isEmpty() )
      drawHeadlineLabel(iter);  // Draw into FTermBuffer object

    ++iter;
  }

  // Print the FTermBuffer object
  drawBufferedHeadline();
}

//----------------------------------------------------------------------
void FListView::drawList()
{
  if ( itemlist.empty() || getHeight() <= 2 || getWidth() <= 4 )
    return;

  uInt y{0};
  const uInt page_height = uInt(getHeight()) - 2;
  const auto& itemlist_end = itemlist.end();
  auto path_end = itemlist_end;
  auto iter = first_visible_line;

  while ( iter != path_end && iter != itemlist_end && y < page_height )
  {
    const bool is_current_line( iter == current_iter );
    const auto& item = static_cast<FListViewItem*>(*iter);
    const int tree_offset = tree_view ? int(item->getDepth() << 1) + 1 : 0;
    const int checkbox_offset = item->isCheckable() ? 1 : 0;
    path_end = getListEnd(item);
    print() << FPoint{2, 2 + int(y)};

    // Draw one FListViewItem
    drawListLine (item, getFlags().focus, is_current_line);

    if ( getFlags().focus && is_current_line )
    {
      int xpos = 3 + tree_offset + checkbox_offset - xoffset;

      if ( xpos < 2 )  // Hide the cursor
        xpos = -9999;  // by moving it outside the visible area

      setVisibleCursor (item->isCheckable());
      setCursorPos ({xpos, 2 + int(y)});  // first character
    }

    last_visible_line = iter;
    y++;
    ++iter;
  }

  // Reset color
  setColor();

  if ( FTerm::isMonochron() )
    setReverse(true);

  // Clean empty space after last element
  while ( y < uInt(getClientHeight()) )
  {
    print() << FPoint{2, 2 + int(y)}
            << FString{std::size_t(getClientWidth()), ' '};
    y++;
  }
}

//----------------------------------------------------------------------
void FListView::drawListLine ( const FListViewItem* item
                             , bool is_focus
                             , bool is_current )
{
  // Set line color and attributes
  setLineAttributes (is_current, is_focus);

  // Print the entry
  const std::size_t indent = item->getDepth() << 1;  // indent = 2 * depth
  FString line{getLinePrefix (item, indent)};

  // Print columns
  if ( ! item->column_list.empty() )
  {
    for (std::size_t col{0}; col < item->column_list.size(); )
    {
      static constexpr std::size_t ellipsis_length = 2;
      const auto& text = item->column_list[col];
      auto width = std::size_t(header[col].width);
      const std::size_t column_width = getColumnWidth(text);
      // Increment the value of col for the column position
      // and the next iteration
      col++;
      const Align align = getColumnAlignment(int(col));
      const std::size_t align_offset = getAlignOffset (align, column_width, width);

      if ( tree_view && col == 1 )
      {
        width -= (indent + 1);

        if ( item->isCheckable() )
          width -= checkbox_space;
      }

      // Insert alignment spaces
      if ( align_offset > 0 )
        line += FString{align_offset, L' '};

      if ( align_offset + column_width <= width )
      {
        // Insert text and trailing space
        static constexpr std::size_t leading_space = 1;
        line += getColumnSubString (text, 1, width);
        line += FString { leading_space + width
                        - align_offset - column_width, L' '};
      }
      else if ( align == Align::Right )
      {
        // Ellipse right align text
        const std::size_t first = getColumnWidth(text) + 1 - width;
        line += FString {L".."};
        line += getColumnSubString (text, first, width - ellipsis_length);
        line += L' ';
      }
      else
      {
        // Ellipse left align text and center text
        line += getColumnSubString (text, 1, width - ellipsis_length);
        line += FString {L".. "};
      }
    }
  }

  const std::size_t width = getWidth() - nf_offset - 2;
  line = getColumnSubString ( line, std::size_t(xoffset) + 1, width );
  const std::size_t len = line.getLength();
  std::size_t char_width{0};

  for (std::size_t i{0}; i < len; i++)
  {
    try
    {
      char_width += getColumnWidth(line[i]);
      print() << line[i];
    }
    catch (const std::out_of_range&)
    {
      return;
    }
  }

  for (std::size_t i = char_width; i < width; i++)
    print (' ');
}

//----------------------------------------------------------------------
void FListView::clearList()
{
  // Clear list from terminal screen

  const auto& wc = getColorTheme();
  setColor (wc->list_fg, wc->list_bg);
  const std::size_t size = getWidth() - 2;
  drawBorder();
  drawHeadlines();

  if ( size == 0 )
    return;

  for (auto y{0}; y < int(getHeight()) - 2; y++)
  {
    print() << FPoint{2, 2 + y} << FString{size, L' '};
  }

  drawScrollbars();
}

//----------------------------------------------------------------------
inline void FListView::setLineAttributes ( bool is_current
                                         , bool is_focus ) const
{
  const auto& wc = getColorTheme();
  setColor (wc->list_fg, wc->list_bg);

  if ( is_current )
  {
    if ( is_focus && FTerm::getMaxColor() < 16 )
      setBold();

    if ( FTerm::isMonochron() )
      unsetBold();

    if ( is_focus )
    {
      setColor ( wc->current_element_focus_fg
               , wc->current_element_focus_bg );
    }
    else
      setColor ( wc->current_element_fg
               , wc->current_element_bg );

    if ( FTerm::isMonochron() )
      setReverse(false);
  }
  else
  {
    if ( FTerm::isMonochron() )
      setReverse(true);
    else if ( is_focus && FTerm::getMaxColor() < 16 )
      unsetBold();
  }
}

//----------------------------------------------------------------------
inline FString FListView::getCheckBox (const FListViewItem* item) const
{
  FString checkbox{""};

  if ( FTerm::isNewFont() )
  {
    checkbox = ( item->isChecked() ) ? CHECKBOX_ON : CHECKBOX;
    checkbox += L' ';
  }
  else
  {
    checkbox.setString("[ ] ");

    if ( item->isChecked() )
    {
      try
      {
        checkbox[1] = wchar_t(UniChar::Times);  // Times ×
      }
      catch (const std::out_of_range&)
      {
        return checkbox;
      }
    }
  }

  return checkbox;
}

//----------------------------------------------------------------------
inline FString FListView::getLinePrefix ( const FListViewItem* item
                                        , std::size_t indent ) const
{
  FString line{""};

  if ( tree_view )
  {
    if ( indent > 0 )
      line = FString{indent, L' '};

    if ( item->isExpandable()  )
    {
      if ( item->isExpand() )
      {
        line += UniChar::BlackDownPointingTriangle;  // ▼
        line += L' ';
      }
      else
      {
        line += UniChar::BlackRightPointingPointer;  // ►
        line += L' ';
      }
    }
    else
      line += L"  ";
  }
  else
    line.setString(" ");

  if ( item->isCheckable() )
    line += getCheckBox(item);

  return line;
}

//----------------------------------------------------------------------
inline void FListView::drawSortIndicator ( std::size_t& length
                                         , std::size_t  column_max )
{
  if ( length >= column_max )
    return;

  setColor();
  length++;

  if ( sort_order == SortOrder::Ascending )
    headerline << UniChar::BlackUpPointingTriangle;    // ▲
  else if ( sort_order == SortOrder::Descending )
    headerline << UniChar::BlackDownPointingTriangle;  // ▼

  if ( length < column_max  )
  {
    length++;
    headerline << ' ';
  }
}

//----------------------------------------------------------------------
inline void FListView::drawHeaderBorder (std::size_t length)
{
  setColor();
  const FString line {length, UniChar::BoxDrawingsHorizontal};
  headerline << line;  // horizontal line
}


//----------------------------------------------------------------------
void FListView::drawHeadlineLabel (const HeaderItems::const_iterator& iter)
{
  // Print label text
  static constexpr std::size_t leading_space = 1;
  const auto& text = iter->name;
  FString txt{" " + text};
  const auto width = std::size_t(iter->width);
  std::size_t column_width = getColumnWidth(txt);
  const std::size_t column_max = leading_space + width;
  const HeaderItems::const_iterator first = header.begin();
  const int column = int(std::distance(first, iter)) + 1;
  const bool has_sort_indicator( sort_column == column && ! hide_sort_indicator );
  const auto& wc = getColorTheme();

  if ( isEnabled() )
    setColor (wc->label_emphasis_fg, wc->label_bg);
  else
    setColor (wc->label_inactive_fg, wc->label_inactive_bg);

  if ( has_sort_indicator && column_width >= column_max - 1 && column_width > 1 )
  {
    column_width = column_max - 2;
    txt = getColumnSubString (txt, 1, column_width);
  }

  if ( column_width <= column_max )
  {
    headerline << txt;

    if ( column_width < column_max )
    {
      column_width++;
      headerline << ' ';  // trailing space
    }

    if ( has_sort_indicator )
      drawSortIndicator (column_width, column_max );

    if ( column_width < column_max )
      drawHeaderBorder (column_max - column_width);
  }
  else
    drawColumnEllipsis (iter, text);  // Print ellipsis
}

//----------------------------------------------------------------------
void FListView::drawBufferedHeadline()
{
  // Print the FTermBuffer object

  if ( headerline.isEmpty() )
    return;

  std::size_t column_offset{0};
  std::size_t column_width{0};
  std::size_t offset{0};
  bool left_truncated_fullwidth{false};
  bool right_truncated_fullwidth{false};
  std::vector<FChar>::const_iterator first{};
  std::vector<FChar>::const_iterator last{};
  last = headerline.end();

  // Search for the start position
  for (auto&& tc : headerline)
  {
    if ( xoffset == 0 )
      break;

    column_offset += getColumnWidth(tc);
    offset++;

    if ( column_offset == std::size_t(xoffset) )
      break;

    if ( column_offset > std::size_t(xoffset) && column_offset >= 2 )
    {
      left_truncated_fullwidth = true;
      break;
    }
  }

  first = headerline.begin();
  std::advance(first, offset);

  // Search for the end position
  if ( getColumnWidth(headerline) > getClientWidth() )
  {
    std::size_t character{0};

    if ( left_truncated_fullwidth )
      column_width++;

    for (auto&& tc : FTermBuffer(first, last))
    {
      const uInt8 char_width = tc.attr.bit.char_width;

      if ( column_width + char_width > getClientWidth() )
      {
        column_width++;
        right_truncated_fullwidth = true;
        break;
      }

      column_width += char_width;
      character++;

      if ( column_width == getClientWidth() )
        break;
    }

    last = first;
    std::advance(last, character);
  }
  else
    column_width = getColumnWidth(headerline);

  // Print the header line
  print() << FPoint{2, 1};

  if ( left_truncated_fullwidth )
    print (UniChar::SingleLeftAngleQuotationMark);  // ‹

  print() << FTermBuffer(first, last);

  if ( right_truncated_fullwidth )
    print (UniChar::SingleRightAngleQuotationMark);  // ›

  while ( column_width < getClientWidth() )
  {
    setColor();
    print(UniChar::BoxDrawingsHorizontal);
    column_width++;
  }
}

//----------------------------------------------------------------------
void FListView::drawColumnEllipsis ( const HeaderItems::const_iterator& iter
                                   , const FString& text )
{
  // Print label ellipsis
  static constexpr int ellipsis_length = 2;
  const int width = iter->width;
  const auto& wc = getColorTheme();

  headerline << ' '
             << getColumnSubString (text, 1, uInt(width - ellipsis_length))
             << FColorPair {wc->label_ellipsis_fg, wc->label_bg}
             << "..";

  if ( iter == header.end() - 1 )  // Last element
    headerline << ' ';
}

//----------------------------------------------------------------------
void FListView::updateDrawing (bool draw_vbar, bool draw_hbar)
{
  if ( isShown() )
    draw();

  vbar->setValue (first_visible_line.getPosition());

  if ( draw_vbar )
    vbar->drawBar();

  hbar->setValue (xoffset);

  if ( draw_hbar )
    hbar->drawBar();

  forceTerminalUpdate();
}

//----------------------------------------------------------------------
std::size_t FListView::determineLineWidth (FListViewItem* item)
{
  static constexpr std::size_t padding_space = 1;
  std::size_t line_width = padding_space;  // leading space
  std::size_t column_idx{0};
  const auto entries = std::size_t(item->column_list.size());

  if ( hasCheckableItems() )
    line_width += checkbox_space;

  for (auto&& header_item : header)
  {
    const auto width = std::size_t(header_item.width);
    const bool fixed_width = header_item.fixed_width;

    if ( ! fixed_width )
    {
      std::size_t len{0};

      if ( column_idx < entries )
        len = getColumnWidth(item->column_list[column_idx]);

      if ( len > width )
        header_item.width = int(len);
    }

    // width + trailing space
    line_width += std::size_t(header_item.width) + padding_space;

    column_idx++;
  }

  return line_width;
}

//----------------------------------------------------------------------
inline void FListView::beforeInsertion (FListViewItem* item)
{
  std::size_t line_width = determineLineWidth (item);
  recalculateHorizontalBar (line_width);
}

//----------------------------------------------------------------------
inline void FListView::afterInsertion()
{
  if ( itemlist.size() == 1 )
  {
    // Select first item on insert
    current_iter = itemlist.begin();
    // The visible area of the list begins with the first element
    first_visible_line = itemlist.begin();
  }

  // Sort list by a column (only if activated)
  sort();

  const std::size_t element_count = getCount();
  recalculateVerticalBar (element_count);
}

//----------------------------------------------------------------------
void FListView::recalculateHorizontalBar (std::size_t len)
{
  if ( len <= max_line_width )
    return;

  max_line_width = len;

  if ( len >= getWidth() - nf_offset - 3 )
  {
    const int hmax = ( max_line_width > getWidth() - nf_offset - 4 )
                     ? int(max_line_width - getWidth() + nf_offset + 4)
                     : 0;
    hbar->setMaximum (hmax);
    hbar->setPageSize (int(max_line_width), int(getWidth() - nf_offset) - 4);
    hbar->calculateSliderValues();

    if ( isShown() )
    {
      if ( isHorizontallyScrollable() )
        hbar->show();
      else
        hbar->hide();
    }
  }
}

//----------------------------------------------------------------------
void FListView::recalculateVerticalBar (std::size_t element_count) const
{
  const std::size_t height = getClientHeight();
  const int vmax = ( element_count > height )
                   ? int(element_count - height)
                   : 0;
  vbar->setMaximum (vmax);
  vbar->setPageSize (int(element_count), int(height));
  vbar->calculateSliderValues();

  if ( isShown() )
  {
    if ( isVerticallyScrollable() )
      vbar->show();
    else
      vbar->hide();
  }
}

//----------------------------------------------------------------------
void FListView::mouseHeaderClicked()
{
  int column{1};
  const int checkbox_offset = ( hasCheckableItems() ) ? checkbox_space : 0;
  const int header_pos = clicked_header_pos.getX() + xoffset;
  int header_start = 2 + checkbox_offset;

  for (auto&& item : header)
  {
    static constexpr int leading_space = 1;
    const bool has_sort_indicator( column == sort_column );
    auto click_width = int(getColumnWidth(item.name));

    if ( has_sort_indicator )
      click_width += 2;

    if ( click_width > item.width )
      click_width = item.width;

    if ( header_pos > header_start
      && header_pos <= header_start + click_width )
    {
      if ( has_sort_indicator && sort_order == SortOrder::Ascending )
        setColumnSort (column, SortOrder::Descending);
      else
        setColumnSort (column, SortOrder::Ascending);

      sort();

      if ( isShown() )
        updateDrawing (true, false);

      break;
    }

    header_start += leading_space + item.width;
    column++;
  }
}

//----------------------------------------------------------------------
void FListView::wheelUp (int pagesize)
{
  if ( itemlist.empty() || current_iter.getPosition() == 0 )
    return;

  if ( first_visible_line.getPosition() >= pagesize )
  {
    current_iter -= pagesize;
    first_visible_line -= pagesize;
    last_visible_line -= pagesize;
  }
  else
  {
    // Save relative position from the first line
    const int ry = current_iter.getPosition() - first_visible_line.getPosition();
    // Save difference from top
    const int difference = first_visible_line.getPosition();
    first_visible_line -= difference;
    last_visible_line -= difference;
    setRelativePosition(ry);
  }
}

//----------------------------------------------------------------------
void FListView::wheelDown (int pagesize)
{
  if ( itemlist.empty() )
    return;

  const auto element_count = int(getCount());

  if ( current_iter.getPosition() + 1 == element_count )
    return;

  if ( last_visible_line.getPosition() < element_count - pagesize )
  {
    current_iter += pagesize;
    first_visible_line += pagesize;
    last_visible_line += pagesize;
  }
  else
  {
    // Save relative position from the first line
    const int ry = current_iter.getPosition() - first_visible_line.getPosition();
    // Save difference from bottom
    const int differenz = element_count - last_visible_line.getPosition() - 1;
    first_visible_line += differenz;
    last_visible_line += differenz;
    setRelativePosition(ry);
  }
}

//----------------------------------------------------------------------
bool FListView::dragScrollUp (int position_before)
{
  if ( position_before == 0 )
  {
    drag_scroll = DragScrollMode::None;
    return false;
  }

  stepBackward(scroll_distance);
  return true;
}

//----------------------------------------------------------------------
bool FListView::dragScrollDown (int position_before)
{
  const auto element_count = int(getCount());

  if ( position_before + 1 == element_count )
  {
    drag_scroll = DragScrollMode::None;
    return false;
  }

  stepForward(scroll_distance);
  return true;
}

//----------------------------------------------------------------------
void FListView::dragUp (MouseButton mouse_button)
{
  if ( drag_scroll != DragScrollMode::None
    && scroll_distance < int(getClientHeight()) )
    scroll_distance++;

  if ( ! scroll_timer && current_iter.getPosition() > 0 )
  {
    scroll_timer = true;
    addTimer(scroll_repeat);

    if ( mouse_button == MouseButton::Right )
      drag_scroll = DragScrollMode::SelectUpward;
    else
      drag_scroll = DragScrollMode::Upward;
  }

  if ( current_iter.getPosition() == 0 )
  {
    delOwnTimers();
    drag_scroll = DragScrollMode::None;
  }
}

//----------------------------------------------------------------------
void FListView::dragDown (MouseButton mouse_button)
{
  if ( drag_scroll != DragScrollMode::None
    && scroll_distance < int(getClientHeight()) )
    scroll_distance++;

  if ( ! scroll_timer && current_iter.getPosition() <= int(getCount()) )
  {
    scroll_timer = true;
    addTimer(scroll_repeat);

    if ( mouse_button == MouseButton::Right )
      drag_scroll = DragScrollMode::SelectDownward;
    else
      drag_scroll = DragScrollMode::Downward;
  }

  if ( current_iter.getPosition() - 1 == int(getCount()) )
  {
    delOwnTimers();
    drag_scroll = DragScrollMode::None;
  }
}

//----------------------------------------------------------------------
void FListView::stopDragScroll()
{
  delOwnTimers();
  scroll_timer = false;
  scroll_distance = 1;
  drag_scroll = DragScrollMode::None;
}

//----------------------------------------------------------------------
FObject::iterator FListView::appendItem (FListViewItem* item)
{
  item->root = root;
  addChild (item);
  itemlist.push_back (item);
  return --itemlist.end();
}

//----------------------------------------------------------------------
void FListView::processClick() const
{
  if ( itemlist.empty() )
    return;

  emitCallback("clicked");
}

//----------------------------------------------------------------------
void FListView::processChanged() const
{
  emitCallback("row-changed");
}

//----------------------------------------------------------------------
void FListView::changeOnResize() const
{
  if ( FTerm::isNewFont() )
  {
    vbar->setGeometry (FPoint{int(getWidth()), 2}, FSize{2, getHeight() - 2});
    hbar->setGeometry (FPoint{1, int(getHeight())}, FSize{getWidth() - 2, 1});
  }
  else
  {
    vbar->setGeometry (FPoint{int(getWidth()), 2}, FSize{1, getHeight() - 2});
    hbar->setGeometry (FPoint{2, int(getHeight())}, FSize{getWidth() - 2, 1});
  }
}

//----------------------------------------------------------------------
inline void FListView::toggleCheckbox()
{
  if ( itemlist.empty() )
    return;

  auto item = getCurrentItem();

  if ( item->isCheckable() )
    item->setChecked(! item->isChecked());
}

//----------------------------------------------------------------------
inline void FListView::collapseAndScrollLeft()
{
  const int position_before = current_iter.getPosition();
  auto item = getCurrentItem();

  if ( xoffset == 0 && item && ! itemlist.empty() )
  {
    if ( tree_view && item->isExpandable() && item->isExpand() )
    {
      // Collapse element
      item->collapse();
      adjustSize();
      vbar->calculateSliderValues();
      // Force vertical scrollbar redraw
      first_line_position_before = -1;
    }
    else if ( item->hasParent() )
    {
      // Jump to parent element
      const auto& parent = item->getParent();

      if ( parent->isInstanceOf("FListViewItem") )
      {
        current_iter.parentElement();

        if ( current_iter.getPosition() < first_line_position_before )
        {
          const int difference = position_before - current_iter.getPosition();

          if ( first_visible_line.getPosition() - difference >= 0 )
          {
            first_visible_line -= difference;
            last_visible_line -= difference;
          }
          else
          {
            const int d = first_visible_line.getPosition();
            first_visible_line -= d;
            last_visible_line -= d;
          }
        }
      }
    }
  }
  else
  {
    // Scroll left
    if ( xoffset > 0 )
      xoffset--;
  }
}

//----------------------------------------------------------------------
inline void FListView::expandAndScrollRight()
{
  const int xoffset_end = int(max_line_width) - int(getClientWidth());
  auto item = getCurrentItem();

  if ( tree_view && ! itemlist.empty() && item
    && item->isExpandable() && ! item->isExpand() )
  {
    // Expand element
    item->expand();
    adjustScrollbars (getCount());
    // Force vertical scrollbar redraw
    first_line_position_before = -1;
  }
  else
  {
    // Scroll right
    if ( xoffset < xoffset_end )
      xoffset++;

    if ( xoffset < 0 )
      xoffset = 0;
  }
}

//----------------------------------------------------------------------
inline void FListView::firstPos()
{
  if ( itemlist.empty() )
    return;

  current_iter -= current_iter.getPosition();
  const int difference = first_visible_line.getPosition();
  first_visible_line -= difference;
  last_visible_line -= difference;
}

//----------------------------------------------------------------------
inline void FListView::lastPos()
{
  if ( itemlist.empty() )
    return;

  const auto element_count = int(getCount());
  current_iter += element_count - current_iter.getPosition() - 1;
  const int difference = element_count - last_visible_line.getPosition() - 1;
  first_visible_line += difference;
  last_visible_line += difference;
}

//----------------------------------------------------------------------
inline bool FListView::expandSubtree()
{
  if ( itemlist.empty() )
    return false;

  auto item = getCurrentItem();

  if ( tree_view && item->isExpandable() && ! item->isExpand() )
  {
    item->expand();
    adjustScrollbars (getCount());
    return true;
  }

  return false;
}

//----------------------------------------------------------------------
inline bool FListView::collapseSubtree()
{
  if ( itemlist.empty() )
    return false;

  auto item = getCurrentItem();

  if ( tree_view && item->isExpandable() && item->isExpand() )
  {
    item->collapse();
    adjustScrollbars (getCount());
    return true;
  }

  return false;
}

//----------------------------------------------------------------------
void FListView::setRelativePosition (int ry)
{
  current_iter = first_visible_line;
  current_iter += ry;
}

//----------------------------------------------------------------------
void FListView::stepForward()
{
  if ( itemlist.empty() )
    return;

  if ( current_iter == last_visible_line )
  {
    ++last_visible_line;

    if ( last_visible_line == itemlist.end() )
      --last_visible_line;
    else
      ++first_visible_line;
  }

  ++current_iter;

  if ( current_iter == itemlist.end() )
    --current_iter;
}

//----------------------------------------------------------------------
void FListView::stepBackward()
{
  if ( itemlist.empty() )
    return;

  if ( current_iter == first_visible_line
    && current_iter != itemlist.begin() )
  {
    --first_visible_line;
    --last_visible_line;
  }

  if ( current_iter != itemlist.begin() )
    --current_iter;
}

//----------------------------------------------------------------------
void FListView::stepForward (int distance)
{
  if ( itemlist.empty() )
    return;

  const auto element_count = int(getCount());

  if ( current_iter.getPosition() + 1 == element_count )
    return;

  if ( current_iter.getPosition() + distance < element_count )
  {
    current_iter += distance;
  }
  else
  {
    current_iter += element_count - current_iter.getPosition() - 1;
  }

  if ( current_iter.getPosition() > last_visible_line.getPosition() )
  {
    if ( last_visible_line.getPosition() + distance < element_count )
    {
      first_visible_line += distance;
      last_visible_line += distance;
    }
    else
    {
      const int differenz = element_count - last_visible_line.getPosition() - 1;
      first_visible_line += differenz;
      last_visible_line += differenz;
    }
  }
}

//----------------------------------------------------------------------
void FListView::stepBackward (int distance)
{
  if ( itemlist.empty() || current_iter.getPosition() == 0 )
    return;

  if ( current_iter.getPosition() - distance >= 0 )
  {
    current_iter -= distance;
  }
  else
  {
    current_iter -= current_iter.getPosition();
  }

  if ( current_iter.getPosition() < first_visible_line.getPosition() )
  {
    if ( first_visible_line.getPosition() - distance >= 0 )
    {
      first_visible_line -= distance;
      last_visible_line -= distance;
    }
    else
    {
      const int difference = first_visible_line.getPosition();
      first_visible_line -= difference;
      last_visible_line -= difference;
    }
  }
}

//----------------------------------------------------------------------
void FListView::scrollToX (int x)
{
  if ( xoffset == x )
    return;

  xoffset = x;
  const int xoffset_end = int(max_line_width) - int(getClientWidth());

  if ( xoffset > xoffset_end )
    xoffset = xoffset_end;

  if ( xoffset < 0 )
    xoffset = 0;
}

//----------------------------------------------------------------------
void FListView::scrollToY (int y)
{
  const int pagesize = int(getClientHeight()) - 1;
  const auto element_count = int(getCount());

  if ( first_visible_line.getPosition() == y )
    return;

  // Save relative position from the top line
  const int ry = current_iter.getPosition() - first_visible_line.getPosition();

  if ( y + pagesize <= element_count )
  {
    first_visible_line = itemlist.begin();
    first_visible_line += y;
    setRelativePosition (ry);
    last_visible_line = first_visible_line;
    last_visible_line += pagesize;
  }
  else
  {
    const int differenz = element_count - last_visible_line.getPosition() - 1;
    current_iter += differenz;
    first_visible_line += differenz;
    last_visible_line += differenz;
  }
}

//----------------------------------------------------------------------
void FListView::scrollTo (int x, int y)
{
  scrollToX(x);
  scrollToY(y);
}

//----------------------------------------------------------------------
void FListView::scrollBy (int dx, int dy)
{
  scrollToX(xoffset + dx);

  if ( dy > 0 )
    stepForward(dy);

  if ( dy < 0 )
    stepBackward(-dy);
}

//----------------------------------------------------------------------
void FListView::cb_vbarChange (const FWidget*)
{
  const FScrollbar::ScrollType scrollType = vbar->getScrollType();
  static constexpr int wheel_distance = 4;
  int distance{1};
  first_line_position_before = first_visible_line.getPosition();
  assert ( scrollType == FScrollbar::ScrollType::None
        || scrollType == FScrollbar::ScrollType::Jump
        || scrollType == FScrollbar::ScrollType::StepBackward
        || scrollType == FScrollbar::ScrollType::StepForward
        || scrollType == FScrollbar::ScrollType::PageBackward
        || scrollType == FScrollbar::ScrollType::PageForward
        || scrollType == FScrollbar::ScrollType::WheelUp
        || scrollType == FScrollbar::ScrollType::WheelDown );

  switch ( scrollType )
  {
    case FScrollbar::ScrollType::None:
      break;

    case FScrollbar::ScrollType::PageBackward:
      distance = int(getClientHeight());
      // fall through
    case FScrollbar::ScrollType::StepBackward:
      stepBackward(distance);
      break;

    case FScrollbar::ScrollType::PageForward:
      distance = int(getClientHeight());
      // fall through
    case FScrollbar::ScrollType::StepForward:
      stepForward(distance);
      break;

    case FScrollbar::ScrollType::Jump:
      scrollToY (vbar->getValue());
      break;

    case FScrollbar::ScrollType::WheelUp:
      wheelUp (wheel_distance);
      break;

    case FScrollbar::ScrollType::WheelDown:
      wheelDown (wheel_distance);
      break;
  }

  if ( isShown() )
    drawList();

  if ( scrollType >= FScrollbar::ScrollType::StepBackward
    && scrollType <= FScrollbar::ScrollType::PageForward )
  {
    vbar->setValue (first_visible_line.getPosition());

    if ( first_line_position_before != first_visible_line.getPosition() )
      vbar->drawBar();

    forceTerminalUpdate();
  }
}

//----------------------------------------------------------------------
void FListView::cb_hbarChange (const FWidget*)
{
  const FScrollbar::ScrollType scrollType = hbar->getScrollType();
  static constexpr int wheel_distance = 4;
  int distance{1};
  const int xoffset_before = xoffset;
  assert ( scrollType == FScrollbar::ScrollType::None
        || scrollType == FScrollbar::ScrollType::Jump
        || scrollType == FScrollbar::ScrollType::StepBackward
        || scrollType == FScrollbar::ScrollType::StepForward
        || scrollType == FScrollbar::ScrollType::PageBackward
        || scrollType == FScrollbar::ScrollType::PageForward
        || scrollType == FScrollbar::ScrollType::WheelUp
        || scrollType == FScrollbar::ScrollType::WheelDown );

  switch ( scrollType )
  {
    case FScrollbar::ScrollType::None:
      break;

    case FScrollbar::ScrollType::PageBackward:
      distance = int(getClientWidth());
      // fall through
    case FScrollbar::ScrollType::StepBackward:
      scrollBy (-distance, 0);
      break;

    case FScrollbar::ScrollType::PageForward:
      distance = int(getClientWidth());
      // fall through
    case FScrollbar::ScrollType::StepForward:
      scrollBy (distance, 0);
      break;

    case FScrollbar::ScrollType::Jump:
      scrollToX (hbar->getValue());
      break;

    case FScrollbar::ScrollType::WheelUp:
      scrollBy (-wheel_distance, 0);
      break;

    case FScrollbar::ScrollType::WheelDown:
      scrollBy (wheel_distance, 0);
      break;
  }

  if ( isShown() )
  {
    drawHeadlines();
    drawList();
  }

  if ( scrollType >= FScrollbar::ScrollType::StepBackward )
  {
    hbar->setValue (xoffset);

    if ( xoffset_before != xoffset )
      hbar->drawBar();

    forceTerminalUpdate();
  }
}

}  // namespace finalcut
