// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
// SPDX-License-Identifier: GPL-3.0-or-later

#include "App/Element/ICPreviewPopup.h"

#include <algorithm>

#include <QFrame>
#include <QGuiApplication>
#include <QScreen>
#include <QVBoxLayout>

#include "App/Core/Settings.h"
#include "App/Element/IC.h"

ICPreviewPopup::ICPreviewPopup(QWidget *parent)
    : QWidget(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
    setAttribute(Qt::WA_TranslucentBackground);
    setAttribute(Qt::WA_NoSystemBackground);   // Prevent solid black fill on Windows
    setAttribute(Qt::WA_ShowWithoutActivating);

    auto *layout = new QVBoxLayout(this);
    layout->setContentsMargins(4, 4, 4, 4);

    // Inset frame holds both the filename caption and the preview image, so the
    // name reads against the same framed background as the rendered circuit
    // rather than floating on the popup chrome outside the frame.
    auto *frame = new QFrame(this);
    frame->setObjectName(QStringLiteral("previewFrame"));
    auto *frameLayout = new QVBoxLayout(frame);
    frameLayout->setContentsMargins(6, 6, 6, 6);
    frameLayout->setSpacing(4);

    m_titleLabel = new QLabel(frame);
    m_titleLabel->setObjectName(QStringLiteral("title"));
    m_titleLabel->setAlignment(Qt::AlignCenter);
    frameLayout->addWidget(m_titleLabel);

    m_imageLabel = new QLabel(frame);
    m_imageLabel->setObjectName(QStringLiteral("preview"));
    m_imageLabel->setAlignment(Qt::AlignCenter);
    frameLayout->addWidget(m_imageLabel);

    layout->addWidget(frame);

    // Outer popup chrome + an inset frame around the caption and pixmap so the
    // content reads as a single framed card rather than floating content.
    // Selectors are scoped by objectName so the inner labels don't inherit the
    // frame's border/background.
    setStyleSheet(
        "ICPreviewPopup {"
        "  background-color: rgba(30, 30, 30, 230);"
        "  border: 1px solid rgba(120, 120, 120, 180);"
        "  border-radius: 6px;"
        "}"
        "QFrame#previewFrame {"
        "  border: 1px solid rgba(170, 170, 170, 200);"
        "  border-radius: 3px;"
        "  background-color: rgba(15, 15, 15, 200);"
        "}"
        "QLabel#title {"
        "  color: rgba(230, 230, 230, 235);"
        "  font-weight: bold;"
        "  padding: 2px 4px;"
        "}"
    );

    m_hideTimer.setSingleShot(true);
    m_hideTimer.setInterval(300);
    connect(&m_hideTimer, &QTimer::timeout, this, &QWidget::hide);

    m_showTimer.setSingleShot(true);
    m_showTimer.setInterval(1000);
    connect(&m_showTimer, &QTimer::timeout, this, &ICPreviewPopup::executeShow);
}

void ICPreviewPopup::showForIC(IC *ic, const QPoint &screenPos)
{
    if (Settings::icPreviewDisabled()) {
        return;
    }

    cancelHide();

    if (!ic) {
        return;
    }

    m_pendingIC = ic;
    m_pendingPos = screenPos;

    if (isVisible()) {
        // If the popup is already visible (e.g., moved quickly from another IC),
        // update it immediately without a delay.
        executeShow();
    } else {
        m_showTimer.start();
    }
}

void ICPreviewPopup::executeShow()
{
    // Resolve the guarded pointer once: QPointer::operator-> re-checks the target
    // on every access, so repeated m_pendingIC-> uses read as a possible null
    // dereference under optimization (-Werror=null-dereference). A single raw
    // pointer after the guard is provably non-null for the rest of the function.
    IC *ic = m_pendingIC;
    if (!ic) {
        return;
    }

    // The popup carries the IC's name (formerly a separate Qt tooltip) plus the
    // rendered preview. The pixmap is null when the IC was empty or oversized at
    // load time, or when generation failed — in that case we still show the name.
    const QString title = ic->displayName();
    const QPixmap &preview = ic->previewPixmap();

    if (title.isEmpty() && preview.isNull()) {
        hide();
        return;
    }

    m_titleLabel->setText(title);
    m_titleLabel->setVisible(!title.isEmpty());

    if (preview.isNull()) {
        m_imageLabel->clear();
        m_imageLabel->hide();
    } else {
        m_imageLabel->setPixmap(preview);
        m_imageLabel->show();
    }

    adjustSize();

    // Position slightly offset from the cursor, then clamp to the available screen
    // geometry so the popup never extends off-screen. Resolve the screen from the cursor
    // itself — the offset position can fall past the edge near a corner, making screenAt()
    // return null and (previously) skipping the clamp entirely.
    QPoint pos = m_pendingPos + QPoint(16, 16);
    if (const auto *screen = QGuiApplication::screenAt(m_pendingPos)) {
        pos = clampedPopupPos(m_pendingPos, size(), screen->availableGeometry());
    }
    move(pos);
    show();
}

QPoint ICPreviewPopup::clampedPopupPos(const QPoint &cursorPos, const QSize &popupSize, const QRect &availableGeometry)
{
    QPoint pos = cursorPos + QPoint(16, 16);
    // Clamp the far edges first, then the near edges, so a popup larger than the available
    // area still anchors to top-left rather than landing off-screen.
    pos.setX((std::min)(pos.x(), availableGeometry.right()  - popupSize.width()));
    pos.setY((std::min)(pos.y(), availableGeometry.bottom() - popupSize.height()));
    pos.setX((std::max)(pos.x(), availableGeometry.left()));
    pos.setY((std::max)(pos.y(), availableGeometry.top()));
    return pos;
}

void ICPreviewPopup::scheduleHide()
{
    m_showTimer.stop();
    m_hideTimer.start();
}

void ICPreviewPopup::cancelHide()
{
    m_hideTimer.stop();
    m_showTimer.stop();
}

void ICPreviewPopup::updatePendingPos(const QPoint &screenPos)
{
    if (!isVisible()) {
        m_pendingPos = screenPos;
    }
}

IC *ICPreviewPopup::pendingIC() const
{
    return m_pendingIC;
}

bool ICPreviewPopup::isShowActiveFor(const IC *ic) const
{
    return m_pendingIC == ic && (isVisible() || m_showTimer.isActive());
}

void ICPreviewPopup::enterEvent(QEnterEvent *event)
{
    Q_UNUSED(event)
    cancelHide();
}

void ICPreviewPopup::leaveEvent(QEvent *event)
{
    Q_UNUSED(event)
    scheduleHide();
}
