/**********************************************************************************************
    Copyright (C) 2026 Oliver Eichler <oliver.eichler@gmx.de>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

**********************************************************************************************/

#include "map/CMapItemDelegate.h"

#include <QMouseEvent>
#include <QPainter>
#include <QToolTip>
#include <QTreeWidget>
#include <QVariantAnimation>

#include "helpers/CDraw.h"
#include "helpers/CRowBuilder.h"
#include "map/IMapItem.h"
#include "misc.h"

constexpr int kFontSizeDiffItem = 2;

CMapItemDelegate::CMapItemDelegate(QTreeWidget* parent) : QStyledItemDelegate(parent), treeWidget(parent) {
  // cross-thread signalling. Signal is triggered by draw thread, indicator animation has to be triggered in main thread
  connect(this, &CMapItemDelegate::sigSetProcessing, this, &CMapItemDelegate::slotSetProcessing);
}

IMapItem* CMapItemDelegate::indexToItem(const QModelIndex& index) const {
  return dynamic_cast<IMapItem*>(treeWidget->itemFromIndex(index));
}

QString CMapItemDelegate::keyFromIndex(const QModelIndex& index) const {
  IMapItem* item = indexToItem(index);
  if (item) {
    return item->getKey();
  }
  return "";
}

void CMapItemDelegate::initAnimation(QPointer<QVariantAnimation>& anim, int duration,
                                     std::function<void(QVariant)> onValueChanged) {
  if (!anim.isNull()) {
    return;
  }
  anim = new QVariantAnimation(this);
  anim->setDuration(duration);
  anim->setEasingCurve(QEasingCurve::InOutQuad);
  connect(anim, &QVariantAnimation::valueChanged, this, std::move(onValueChanged));
}

CMapItemDelegate::animations_t& CMapItemDelegate::getAnimations(const QModelIndex& index) {
  const QString& key = keyFromIndex(index);
  animations_t& anim = data[key].animations;

  initAnimation(anim.animColorIndicator, 250, [this, key](QVariant v) {
    data[key].animations.colorIndicator = v.value<QColor>();
    emit sigUpdateItem(key);
  });
  initAnimation(anim.animOpacityIndicator, 250, [this, key](QVariant v) {
    data[key].animations.opacityIndicator = v.toFloat();
    emit sigUpdateItem(key);
  });
  initAnimation(anim.animFlashingIndicator, 500, [this, key](QVariant v) {
    data[key].animations.opacityIndicator = v.toFloat();
    emit sigUpdateItem(key);
  });
  initAnimation(anim.animAccessInfo, 2000, [this, key](QVariant v) {
    data[key].animations.opacityAccessInfo = v.toFloat();
    emit sigUpdateItem(key);
  });

  return anim;
}

const CMapItemDelegate::animations_t& CMapItemDelegate::getAnimations(const QModelIndex& index) const {
  return data[keyFromIndex(index)].animations;
}

void CMapItemDelegate::setColor(const QModelIndex& index, const QColor& c) {
  animations_t& anim = getAnimations(index);
  if (anim.colorIndicator == c) {
    return;
  }

  anim.animColorIndicator->stop();
  anim.animColorIndicator->setStartValue(anim.colorIndicator);
  anim.animColorIndicator->setEndValue(c);
  anim.animColorIndicator->start();
}

void CMapItemDelegate::hideIndicator(const QModelIndex& index) {
  animations_t& anim = getAnimations(index);
  if (anim.opacityIndicator == 0.0) {
    return;
  }

  anim.animFlashingIndicator->stop();

  anim.animOpacityIndicator->stop();
  anim.animOpacityIndicator->setStartValue(anim.opacityIndicator);
  anim.animOpacityIndicator->setEndValue(0.0);
  anim.animOpacityIndicator->start();
}

void CMapItemDelegate::showIndicator(const QModelIndex& index) {
  animations_t& anim = getAnimations(index);
  if (anim.opacityIndicator == 1.0) {
    return;
  }

  anim.animFlashingIndicator->stop();

  anim.animOpacityIndicator->stop();
  anim.animOpacityIndicator->setStartValue(anim.opacityIndicator);
  anim.animOpacityIndicator->setEndValue(1.0);
  anim.animOpacityIndicator->start();
}

void CMapItemDelegate::setProcessing(const QModelIndex& index, bool on) {
  // use signal to decouple threads when setProcessing()
  // is called from a thread not the main application thread
  emit sigSetProcessing(index, on);
}

void CMapItemDelegate::slotSetProcessing(const QModelIndex& index, bool on) {
  animations_t& anim = getAnimations(index);

  anim.animOpacityIndicator->stop();

  anim.animFlashingIndicator->stop();
  if (on) {
    anim.animFlashingIndicator->setStartValue(1.0);
    anim.animFlashingIndicator->setEndValue(0.0);
    anim.animFlashingIndicator->setLoopCount(-1);
    anim.animFlashingIndicator->setDirection(QAbstractAnimation::Forward);
    anim.animFlashingIndicator->start();
  } else {
    IMapItem* item = indexToItem(index);
    if (item == nullptr || item->getStatus() != IMapItem::eStatus::Active) {
      hideIndicator(index);
    } else {
      showIndicator(index);
    }
  }
}

void CMapItemDelegate::setAccess(const QModelIndex& index, const QString& info) {
  animations_t& anim = getAnimations(index);

  anim.animAccessInfo->stop();
  anim.accessInfo = info;
  anim.opacityAccessInfo = 1.0;
  anim.animAccessInfo->setStartValue(anim.opacityAccessInfo);
  anim.animAccessInfo->setEndValue(0.0);
  anim.animAccessInfo->start();
}

void CMapItemDelegate::initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const {
  QStyledItemDelegate::initStyleOption(option, index);

  const QString& key = keyFromIndex(index);
  if (!data.contains(key)) {
    data.insert(key, {});
  }
  data[key].icon = option->icon;
  option->icon = QIcon();
  option->decorationSize = QSize(0, 0);
}

CMapItemDelegate::MapItemLayout CMapItemDelegate::getRectangles(const QStyleOptionViewItem& opt) const {
  MapItemLayout layout;
  layout.fontName = opt.font;
  const QFontMetrics fmName(layout.fontName);

  layout.fontStatus = opt.font;
  layout.fontStatus.setPointSize(layout.fontStatus.pointSize() - kFontSizeDiffItem);
  const QFontMetrics fmStatus(layout.fontStatus);

  CRowBuilder row(opt.rect, kCellPad, kInnerGap);
  layout.rectIcon = row.takeLeft(row.height());
  layout.rectButton = row.takeRight(row.height());
  // Thin vertical indicator bar to the left of the button.
  const QRect rawIndicatorSlot = row.takeRight(6);
  layout.rectIndicator = rawIndicatorSlot.adjusted(0, kMargin, 0, -kMargin);
  layout.rectName = row.nameSlice(fmName.height());
  layout.rectStatus = row.statusSlice(fmStatus.height());

  return layout;
}

QRect CMapItemDelegate::overviewBadgeRect(const QRect& rectIcon) {
  const int size = qRound(rectIcon.width() * 2.0 / 3.0);
  return QRect(rectIcon.right() - size + 1, rectIcon.bottom() - size + 1, size, size);
}

void CMapItemDelegate::paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& index) const {
  IMapItem* item = indexToItem(index);
  if (item == nullptr) {
    return;
  }

  p->save();
  USE_ANTI_ALIASING(*p, true);
  // Standard background & selection
  QStyledItemDelegate::paint(p, opt, index);

  const bool isActive = item->getStatus() == IMapItem::eStatus::Active;

  // derive strings colors
  const QColor colorName = CDraw::itemNameColor(opt, isActive);

  QColor colorStatus = colorName;
  QString status;
  switch (item->getStatus()) {
    case IMapItem::eStatus::Inactive:
      status = tr("inactive");
      break;
    case IMapItem::eStatus::Active:
      status = tr("active");
      break;
    case IMapItem::eStatus::Missing:
      status = "!!!" + tr("missing") + "!!!";
      colorStatus = Qt::red;
      break;
    case IMapItem::eStatus::Unused:
      status = tr("-");
      break;
  }

  // derive all rectangles to place visual elements
  auto layout = getRectangles(opt);
  layout.fontName.setBold(isActive);

  // draw name
  p->setPen(colorName);
  p->setFont(layout.fontName);
  p->drawText(layout.rectName.adjusted(0, -1, 0, 1), Qt::AlignLeft | Qt::AlignVCenter, item->getName());
  p->setClipping(false);

  // draw status
  p->setPen(colorStatus);
  p->setFont(layout.fontStatus);
  p->drawText(layout.rectStatus.adjusted(0, -1, 0, 1), Qt::AlignLeft | Qt::AlignVCenter, status);

  // draw icon
  data[keyFromIndex(index)].icon.paint(p, layout.rectIcon);

  // draw overview-warning badge over the icon, if the item's active data source needs it
  if (item->showsOverviewWarning()) {
    QIcon(":/icons/Attention.svgt").paint(p, overviewBadgeRect(layout.rectIcon));
  }

  // draw tool button to activate
  CDraw::drawToolButton(p, opt, layout.rectButton,
                        isActive ? QIcon(":/icons/ShowAll.svgt") : QIcon(":/icons/ShowNone.svgt"),
                        item->getStatus() != IMapItem::eStatus::Missing, isActive);

  // draw all elements that tinker with opacity
  // draw indicator
  const animations_t& anim = getAnimations(index);
  p->setOpacity(anim.opacityIndicator);
  p->setPen(Qt::NoPen);
  p->setBrush(anim.colorIndicator);
  p->drawRoundedRect(layout.rectIndicator, 4, 4);

  // draw access info
  p->setPen(colorName);
  p->setOpacity(anim.opacityAccessInfo);
  p->setFont(layout.fontStatus);
  p->drawText(layout.rectStatus, Qt::AlignRight | Qt::AlignVCenter, anim.accessInfo);

  p->restore();
}

bool CMapItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& opt,
                                   const QModelIndex& index) {
  if (event->type() == QEvent::MouseButtonPress) {
    auto* me = static_cast<QMouseEvent*>(event);

    const auto& layout = getRectangles(opt);

    IMapItem* item = indexToItem(index);
    if (item != nullptr && item->showsOverviewWarning() && overviewBadgeRect(layout.rectIcon).contains(me->pos())) {
      item->triggerOverviewAdvisory();
      return true;
    }

    if (layout.rectButton.contains(me->pos())) {
      if (item == nullptr) {
        return false;
      }
      if (item->getStatus() != IMapItem::eStatus::Missing) {
        const bool activate = item->getStatus() != IMapItem::eStatus::Active;
        item->activate(activate);
        // Reconcile the indicator with the real outcome: activation can fail (e.g. a
        // rejected VRT) and leave the item inactive, so the bar must follow the final
        // status rather than the optimistic click intent.
        if (item->getStatus() == IMapItem::eStatus::Active) {
          showIndicator(index);
        } else {
          hideIndicator(index);
        }
      }
      return true;
    }
  }

  return QStyledItemDelegate::editorEvent(event, model, opt, index);
}

bool CMapItemDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& opt,
                                 const QModelIndex& index) {
  if (!event || !view) {
    return false;
  }
  IMapItem* item = indexToItem(index);
  if (item == nullptr) {
    return false;
  }

  const bool isActive = item->getStatus() == IMapItem::eStatus::Active;
  const auto& layout = getRectangles(opt);

  if (item->showsOverviewWarning() && overviewBadgeRect(layout.rectIcon).contains(event->pos())) {
    QToolTip::showText(
        event->globalPos(),
        toRichText(tr("This file can be slow to draw on the map. Click here to see how to speed it up.")), view, {},
        5000);
  } else if (layout.rectButton.contains(event->pos())) {
    const QString& tip = isActive ? tr("Deactivate %1").arg(item->getName()) : tr("Activate %1").arg(item->getName());
    QToolTip::showText(event->globalPos(), toRichText(tip), view, {}, 3000);
  } else if (isActive && layout.rectIndicator.contains(event->pos())) {
    const bool outOfScale = item->isOutOfScale();
    const QString& tip = outOfScale ? tr("%1 is not visible at current scale").arg(item->getName())
                                    : tr("%1 is visible at current scale").arg(item->getName());
    QToolTip::showText(event->globalPos(), toRichText(tip), view, {}, 3000);
  } else if (layout.rectName.contains(event->pos())) {
    const QFontMetrics fm(layout.fontName);
    const QRect& boundingRectName = fm.boundingRect(item->getName());
    QString toolTip;
    if (boundingRectName.width() > layout.rectName.width()) {
      toolTip = QString("<p>%1</p>").arg(item->getName());
    }
    if (!item->getToolTip().isEmpty()) {
      toolTip += QString("<p>%1</p>").arg(item->getToolTip());
    }
    if (!toolTip.isEmpty()) {
      QToolTip::showText(event->globalPos(), toolTip, view, {}, 5000);
    }
  }

  return true;
}

QSize CMapItemDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelIndex& index) const {
  const QFontMetrics fmName(opt.font);

  QFont fontStatus = opt.font;
  fontStatus.setPointSize(fontStatus.pointSize() - kFontSizeDiffItem);
  const QFontMetrics fmStatus(fontStatus);

  return QSize(opt.rect.width(), std::max(22, CRowBuilder::rowHeight(kCellPad, fmName.height(), fmStatus.height())));
}
