#include <QScrollBar>
#include <QPainter>

#include "gridview.h"
#include "../ui_qt.h"

/* http://www.informit.com/articles/article.aspx?p=1613548 */

ThumbnailDelegate::ThumbnailDelegate(const GridItem &gridItem, QObject* parent) :
   QStyledItemDelegate(parent), m_style(gridItem) { }

void ThumbnailDelegate::paint(QPainter* painter,
      const QStyleOptionViewItem &option, const QModelIndex& index) const
{
   QStyleOptionViewItem opt = option;
   const QWidget    *widget = opt.widget;
   QStyle            *style = widget->style();
   int              padding = m_style.padding;
   int      text_top_margin = 4; /* Qt seemingly reports -4 the actual line height. */
   int          text_height = painter->fontMetrics().height() + padding + padding;
   QRect               rect = opt.rect;
   QRect           adjusted = rect.adjusted(padding, padding, -padding,
         -text_height + text_top_margin);
   QPixmap           pixmap = index.data(
         PlaylistModel::THUMBNAIL).value<QPixmap>();

   painter->save();

   initStyleOption(&opt, index);

   /* draw the background */
   style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, widget);

   /* draw the image */
   if (!pixmap.isNull())
   {
      QPixmap pixmapScaled = pixmap.scaled(adjusted.size(),
            Qt::KeepAspectRatio, Qt::SmoothTransformation);
      style->drawItemPixmap(painter, adjusted,
            Qt::AlignHCenter | m_style.thumbnailVerticalAlignmentFlag,
            pixmapScaled);
   }

   /* draw the text */
   if (!opt.text.isEmpty())
   {
      QPalette::ColorGroup cg = opt.state & QStyle::State_Enabled
         ? QPalette::Normal : QPalette::Disabled;
      QRect textRect          = QRect(rect.x() + padding,
            rect.y() + adjusted.height() - text_top_margin + padding,
            rect.width() - 2 * padding, text_height);
      QString elidedText      = painter->fontMetrics().elidedText(opt.text,
            opt.textElideMode, textRect.width(), Qt::TextShowMnemonic);

      if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active))
         cg = QPalette::Inactive;

      if (opt.state & QStyle::State_Selected)
         painter->setPen(opt.palette.color(cg, QPalette::HighlightedText));
      else
         painter->setPen(opt.palette.color(cg, QPalette::Text));

      painter->setFont(opt.font);
      painter->drawText(textRect, Qt::AlignCenter, elidedText);
   }

   painter->restore();
}

GridView::GridView(QWidget *parent) :
   QAbstractItemView(parent), m_idealHeight(0), m_hashIsDirty(false)
{
   setFocusPolicy(Qt::WheelFocus);
   horizontalScrollBar()->setRange(0, 0);
   verticalScrollBar()->setRange(0, 0);
}

void GridView::setModel(QAbstractItemModel *newModel)
{
   if (model())
      disconnect(model(), SIGNAL(rowsRemoved(QModelIndex, int, int)),
            this, SLOT(rowsRemoved(QModelIndex, int, int)));

   QAbstractItemView::setModel(newModel);

   connect(newModel, SIGNAL(rowsRemoved(QModelIndex, int, int)),
         this, SLOT(rowsRemoved(QModelIndex, int, int)));

   m_hashIsDirty = true;
}

void GridView::setviewMode(ViewMode mode)
{
   m_viewMode = mode;
}

void GridView::calculateRectsIfNecessary() const
{
   int x, y;
   int row, nextX;
   if (!m_hashIsDirty)
      return;

   x                  = m_spacing;
   y                  = m_spacing;
   const int maxWidth = viewport()->width();

   if (m_size + m_spacing * 2 > maxWidth)
   {
      m_rectForRow[0] = QRectF(x, y, m_size, m_size);

      for (row = 1; row < model()->rowCount(); ++row)
      {
         y                 += m_size + m_spacing;
         m_rectForRow[row]  = QRectF(x, y, m_size, m_size);
      }
   }
   else
   {
      switch (m_viewMode)
      {
         case Anchored:
         {
            int columns = (maxWidth - m_spacing) / (m_size + m_spacing);
            if (columns > 0)
            {
               const int actualSpacing = (maxWidth - m_spacing -
                     m_size - (columns - 1) * m_size) / columns;
               for (row = 0; row < model()->rowCount(); ++row)
               {
                  nextX = x + m_size + actualSpacing;
                  if (nextX > maxWidth)
                  {
                     x = m_spacing;
                     y += m_size + m_spacing;
                     nextX = x + m_size + actualSpacing;
                  }
                  m_rectForRow[row] = QRectF(x, y, m_size, m_size);
                  x = nextX;
               }
            }
            break;
         }
         case Centered:
         {
            int columns = (maxWidth - m_spacing) / (m_size + m_spacing);
            if (columns > 0)
            {
               const int actualSpacing = (maxWidth - columns * m_size)
                  / (columns + 1);
               x                       = actualSpacing;

               for (row = 0; row < model()->rowCount(); ++row)
               {
                  nextX = x + m_size + actualSpacing;
                  if (nextX > maxWidth)
                  {
                     x = actualSpacing;
                     y += m_size + m_spacing;
                     nextX = x + m_size + actualSpacing;
                  }
                  m_rectForRow[row] = QRectF(x, y, m_size, m_size);
                  x = nextX;
               }
            }
            break;
         }
         case Simple:
            for (row = 0; row < model()->rowCount(); ++row)
            {
               nextX = x + m_size + m_spacing;
               if (nextX > maxWidth)
               {
                  x = m_spacing;
                  y += m_size + m_spacing;
                  nextX = x + m_size + m_spacing;
               }
               m_rectForRow[row] = QRectF(x, y, m_size, m_size);
               x = nextX;
            }
            break;
         }
   }

   m_idealHeight = y + m_size + m_spacing;
   m_hashIsDirty = false;
   viewport()->update();
}

QRect GridView::visualRect(const QModelIndex &index) const
{
   QRect rect;
   if (index.isValid())
      rect = viewportRectForRow(index.row()).toRect();
   return rect;
}

QRectF GridView::viewportRectForRow(int row) const
{
   QRectF rect;
   calculateRectsIfNecessary();
   rect = m_rectForRow.value(row).toRect();
   if (!rect.isValid())
      return rect;
   return QRectF(
         rect.x() - horizontalScrollBar()->value(),
         rect.y() - verticalScrollBar()->value(),
         rect.width(),
         rect.height());
}

void GridView::scrollTo(const QModelIndex &index, QAbstractItemView::ScrollHint)
{
   QRect viewRect = viewport()->rect();
   QRect itemRect = visualRect(index);

   if (itemRect.left() < viewRect.left())
      horizontalScrollBar()->setValue(horizontalScrollBar()->value()
            + itemRect.left() - viewRect.left());
   else if (itemRect.right() > viewRect.right())
      horizontalScrollBar()->setValue(horizontalScrollBar()->value()
            + qMin(itemRect.right() - viewRect.right(),
               itemRect.left() - viewRect.left()));
   if (itemRect.top() < viewRect.top())
      verticalScrollBar()->setValue(verticalScrollBar()->value()
            + itemRect.top() - viewRect.top());
   else if (itemRect.bottom() > viewRect.bottom())
      verticalScrollBar()->setValue(verticalScrollBar()->value()
            + qMin(itemRect.bottom() - viewRect.bottom(),
               itemRect.top() - viewRect.top()));
   viewport()->update();
}

/* TODO: Make this more efficient by changing m_rectForRow for
 * another data structure. Look at how Qt's own views do it. */
QModelIndex GridView::indexAt(const QPoint &point_) const
{
   QPoint point(point_);
   QHash<int, QRectF>::const_iterator i;
   point.rx() += horizontalScrollBar()->value();
   point.ry() += verticalScrollBar()->value();

   calculateRectsIfNecessary();

   i = m_rectForRow.constBegin();

   while (i != m_rectForRow.constEnd())
   {
      if (i.value().contains(point))
         return model()->index(i.key(), 0, rootIndex());
      i++;
   }
   return QModelIndex();
}

void GridView::dataChanged(const QModelIndex &topLeft,
      const QModelIndex &bottomRight, const QVector<int> &roles)
{
   m_hashIsDirty = true;
   QAbstractItemView::dataChanged(topLeft, bottomRight);
}

void GridView::refresh()
{
   m_hashIsDirty = true;
   calculateRectsIfNecessary();
   updateGeometries();
}

void GridView::rowsInserted(const QModelIndex &parent, int start, int end)
{
   QAbstractItemView::rowsInserted(parent, start, end);
   refresh();
}

void GridView::rowsRemoved(const QModelIndex &parent, int start, int end)
{
   refresh();
}

void GridView::setGridSize(const int newSize)
{
   if (newSize != m_size)
   {
      m_size = newSize;
      refresh();
   }
}

void GridView::resizeEvent(QResizeEvent*)
{
   refresh();
}

void GridView::reset()
{
   m_visibleIndexes.clear();
   QAbstractItemView::reset();
   refresh();
}

QModelIndex GridView::moveCursor(QAbstractItemView::CursorAction cursorAction,
      Qt::KeyboardModifiers)
{
   QModelIndex index = currentIndex();
   if (index.isValid())
   {
      if (     (cursorAction == MoveLeft  && index.row() > 0)
            || (cursorAction == MoveRight && index.row() + 1 < model()->rowCount()))
      {
         const int offset = (cursorAction == MoveLeft ? -1 : 1);
         index = model()->index(index.row() + offset, index.column(), index.parent());
      }
      else if ((cursorAction == MoveUp   && index.row() > 0)
            || (cursorAction == MoveDown && index.row() + 1 < model()->rowCount()))
      {
         const int offset = ((m_size + m_spacing) * (cursorAction == MoveUp ? -1 : 1));
         QRect rect = viewportRectForRow(index.row()).toRect();
         QPoint point(rect.center().x(), rect.center().y() + offset);
         index = indexAt(point);
      }
   }
   return index;
}

int GridView::horizontalOffset() const
{
   return horizontalScrollBar()->value();
}

int GridView::verticalOffset() const
{
   return verticalScrollBar()->value();
}

void GridView::scrollContentsBy(int dx, int dy)
{
   scrollDirtyRegion(dx, dy);
   viewport()->scroll(dx, dy);
   emit(visibleItemsChangedMaybe());
}

/* TODO: Maybe add a way to get the previous/next visible indexes. */
QVector<QModelIndex> GridView::visibleIndexes() const {
   return m_visibleIndexes;
}

void GridView::setSelection(const QRect &rect,
      QFlags<QItemSelectionModel::SelectionFlag> flags)
{
   QRect rectangle;
   QHash<int, QRectF>::const_iterator i;
   int firstRow = model()->rowCount();
   int lastRow  = -1;

   calculateRectsIfNecessary();

   rectangle    = rect.translated(horizontalScrollBar()->value(),
         verticalScrollBar()->value()).normalized();

   i = m_rectForRow.constBegin();

   while (i != m_rectForRow.constEnd())
   {
      if (i.value().intersects(rectangle))
      {
         firstRow = firstRow < i.key() ? firstRow : i.key();
         lastRow = lastRow > i.key() ? lastRow : i.key();
      }
      i++;
   }
   if (firstRow != model()->rowCount() && lastRow != -1)
   {
      QItemSelection selection(model()->index(
               firstRow, 0, rootIndex()),
            model()->index(lastRow, 0, rootIndex()));
      selectionModel()->select(selection, flags);
   }
   else
   {
      QModelIndex invalid;
      QItemSelection selection(invalid, invalid);
      selectionModel()->select(selection, flags);
   }
}

QRegion GridView::visualRegionForSelection(const QItemSelection &selection) const
{
   int i;
   QRegion region;
   QItemSelectionRange range;

   for (i = 0; i < selection.size(); i++)
   {
      range = selection.at(i);
      int row;
      for (row = range.top(); row <= range.bottom(); row++)
      {
         int column;
         for (column = range.left(); column < range.right(); column++)
         {
            QModelIndex index = model()->index(row, column, rootIndex());
            region += visualRect(index);
         }
      }
   }

   return region;
}

void GridView::paintEvent(QPaintEvent*)
{
   QPainter painter(viewport());
   int row;

   painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
   m_visibleIndexes.clear();

   for (row = 0; row < model()->rowCount(rootIndex()); ++row)
   {
      QModelIndex index = model()->index(row, 0, rootIndex());
      QRectF rect = viewportRectForRow(row);
#if (QT_VERSION > QT_VERSION_CHECK(6, 0, 0))
      QStyleOptionViewItem option;
      initViewItemOption(&option);
#else
      QStyleOptionViewItem option = viewOptions();
#endif

      if (    !rect.isValid()
            || rect.bottom() < 0
            || rect.y() > viewport()->height())
         continue;

      m_visibleIndexes.append(index);
      option.rect = rect.toRect();

      if (selectionModel()->isSelected(index))
         option.state |= QStyle::State_Selected;

      if (currentIndex() == index)
         option.state |= QStyle::State_HasFocus;

      itemDelegate()->paint(&painter, option, index);
   }
}

void GridView::updateGeometries()
{
   const int RowHeight = m_size + m_spacing;

   QAbstractItemView::updateGeometries();

   verticalScrollBar()->setSingleStep(RowHeight);
   verticalScrollBar()->setPageStep(viewport()->height());
   verticalScrollBar()->setRange(0, qMax(0,
            m_idealHeight - viewport()->height()));

   horizontalScrollBar()->setPageStep(viewport()->width());
   horizontalScrollBar()->setRange(0, qMax(0,
            RowHeight - viewport()->width()));

   emit(visibleItemsChangedMaybe());
}

QString GridView::getLayout() const
{
   switch (m_viewMode)
   {
      case Simple:
         return QStringLiteral("simple");
      case Anchored:
         return QStringLiteral("anchored");
      case Centered:
      default:
         break;
   }

   return QStringLiteral("centered");
}

void GridView::setLayout(QString layout)
{
   if (layout == QLatin1String("anchored"))
      m_viewMode = Anchored;
   else if (layout == QLatin1String("centered"))
      m_viewMode = Centered;
   else if (layout == QLatin1String("fixed"))
      m_viewMode = Simple;
}

int GridView::getSpacing() const
{
   return m_spacing;
}

void GridView::setSpacing(const int spacing) { m_spacing = spacing; }

GridItem::GridItem(QWidget* parent) : QWidget(parent)
, thumbnailVerticalAlignmentFlag(Qt::AlignBottom)
, padding(11) { }

int GridItem::getPadding() const { return padding; }
void GridItem::setPadding(const int value) { padding = value; }

QString GridItem::getThumbnailVerticalAlign() const
{
   switch (thumbnailVerticalAlignmentFlag)
   {
      case Qt::AlignTop:
         return QStringLiteral("top");
      case Qt::AlignVCenter:
         return QStringLiteral("center");
      case Qt::AlignBottom:
      default:
         break;
   }

   return QStringLiteral("bottom");
}

void GridItem::setThumbnailVerticalAlign(const QString valign)
{
   if (valign == QLatin1String("top"))
      thumbnailVerticalAlignmentFlag = Qt::AlignTop;
   else if (valign == QLatin1String("center"))
      thumbnailVerticalAlignmentFlag = Qt::AlignVCenter;
   else if (valign == QLatin1String("bottom"))
      thumbnailVerticalAlignmentFlag = Qt::AlignBottom;
}
