// Sane Break is a gentle break reminder that helps you avoid mindlessly skipping breaks
// Copyright (C) 2024-2026 Sane Break developers
// SPDX-License-Identifier: GPL-3.0-or-later

#include "break-window.h"

#include <qglobal.h>

#include <QApplication>
#include <QColor>
#include <QFile>
#include <QGraphicsOpacityEffect>
#include <QIODevice>
#include <QLabel>
#include <QMainWindow>
#include <QPauseAnimation>
#include <QPixmap>
#include <QProgressBar>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QRect>
#include <QScreen>
#include <QSequentialAnimationGroup>
#include <QSize>
#include <QString>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
#include <QWidget>
#include <QWindow>
#include <Qt>
#include <algorithm>

#include "config.h"
#include "core/break-windows.h"
#include "lib/utils.h"
#include "ui_break-window.h"

#ifdef Q_OS_LINUX
#include "lib/linux/system-check.h"
#endif  // Q_OS_LINUX
#ifdef Q_OS_MACOS
#include "lib/macos/workspace.h"
#endif

const int BreakWindow::SMALL_WINDOW_WIDTH = 400;
const int BreakWindow::SMALL_WINDOW_HEIGHT = 120;

BreakWindow::BreakWindow(BreakWindowData data, QWidget* parent)
    : QMainWindow(parent), m_data(data) {
#ifdef Q_OS_LINUX
  // Positioning windows on Wayland is nearly impossible without layer shell protol.
  // In Wayland workaround mode, the main window is transparent and takes up all
  // available space on the screen (but not the system panels), and the main widget
  // changes size and covers the space.
  m_waylandWorkaround =
      QGuiApplication::platformName() == "wayland" && !LinuxSystemSupport::layerShell;
  // X11 does not support changing WindowTransparentForInput flag afterwards.
  // Therefore, we just don't set the WindowTransparentForInput flag.
  m_supportTransparentInput = QGuiApplication::platformName() == "wayland";
#endif
  setAttribute(Qt::WA_TranslucentBackground);    // transparent window
  setAttribute(Qt::WA_ShowWithoutActivating);    // avoid gaining keyboard focus
  setAttribute(Qt::WA_LayoutOnEntireRect);       // ignore safe zone on macOS
  setAttribute(Qt::WA_MacAlwaysShowToolWindow);  // always show window on macOS
  setWindowFlags(Qt::WindowDoesNotAcceptFocus | Qt::FramelessWindowHint |
                 Qt::WindowStaysOnTopHint);
#ifdef Q_OS_MACOS  // Use Qt::ToolTip on macOS to display above menubars
  setWindowFlag(Qt::ToolTip);
#else  // Use Qt::Tool on other platforms to avoid glitches in #79
  setWindowFlag(Qt::Tool);
#endif
  if (m_supportTransparentInput) setWindowFlag(Qt::WindowTransparentForInput);
  setWindowTitle("Break reminder - Sane Break");

  mainWidget = new QWidget(this);
  if (!m_waylandWorkaround) setCentralWidget(mainWidget);

  ui = new Ui::BreakReminder();
  ui->setupUi(mainWidget);
  mainWidget->resize(0, 0);
  mainWidget->setStyleSheet(
      QString("BreakWindow QLabel { color: %1; }"
              "BreakWindow #countdownLabel { color: %2; }"
              "BreakWindow #breakEndTimeLabel { color: %2; }"
              "BreakWindow QProgressBar::chunk { background: %2; }")
          .arg(data.theme.messageColor.name(QColor::HexArgb),
               data.theme.countDownColor.name(QColor::HexArgb)));
  mainWidget->setProperty("isFullScreen", false);
  mainWidget->setAttribute(Qt::WA_LayoutOnEntireRect);

  QColor hoverColor = data.theme.messageColor;
  hoverColor.setAlpha(40);
  ui->buttons->setStyleSheet(
      ui->buttons->styleSheet().replace("#aaaaaaaa", hoverColor.name(QColor::HexArgb)));
  colorizeButton(ui->lockScreen, data.theme.messageColor);
  colorizeButton(ui->exitForceBreak, data.theme.messageColor);
  ui->lockScreenGroup->setVisible(false);
  ui->exitForceBreakGroup->setVisible(false);
  connect(ui->lockScreen, &QPushButton::pressed, this,
          &BreakWindow::lockScreenRequested);
  connect(ui->exitForceBreak, &QPushButton::pressed, this,
          &BreakWindow::exitForceBreakRequested);

  ui->postponeLabel->setVisible(false);

  m_progressAnim = new QPropertyAnimation(ui->progressBar, "value", this);
  m_totalSeconds = data.totalSeconds;

  int baseDuration = data.theme.flashAnimationDuration;
  if (baseDuration <= 0) {
    setColor(data.theme.mainBackground);
    return;
  }
  auto* bgAnimGroup = new QSequentialAnimationGroup(this);
  int flashCount = std::max({2, 10000 / baseDuration});

  // Flash burst
  for (int i = 0; i < flashCount; ++i) {
    auto* flash = new QPropertyAnimation(this, "color");
    flash->setStartValue(data.theme.highlightBackground);
    flash->setEndValue(data.theme.mainBackground);
    flash->setDuration(baseDuration);
    bgAnimGroup->addAnimation(flash);
  }

  // Fade to semi-transparent quiet state
  QColor quietColor = data.theme.mainBackground;
  quietColor.setAlphaF(quietColor.alphaF() * 0.5);
  auto* fadeOut = new QPropertyAnimation(this, "color");
  fadeOut->setStartValue(data.theme.mainBackground);
  fadeOut->setEndValue(quietColor);
  fadeOut->setDuration(500);
  bgAnimGroup->addAnimation(fadeOut);

  // Hold quiet state
  bgAnimGroup->addAnimation(new QPauseAnimation(6000));

  // Fade back to prepare for next flash burst
  auto* fadeIn = new QPropertyAnimation(this, "color");
  fadeIn->setStartValue(quietColor);
  fadeIn->setEndValue(data.theme.mainBackground);
  fadeIn->setDuration(300);
  bgAnimGroup->addAnimation(fadeIn);

  bgAnimGroup->setLoopCount(-1);
  m_bgAnim = bgAnimGroup;
  m_bgAnim->start();
}

QColor BreakWindow::color() const { return backgroundColor; }

void BreakWindow::setColor(const QColor& color) {
  if (backgroundColor == color) return;
  backgroundColor = color;
  setStyleSheet(QString("BreakWindow, BreakWindow #BreakReminder { background: "
                        "rgba(%1, %2, %3, %4); }")
                    .arg(backgroundColor.red())
                    .arg(backgroundColor.green())
                    .arg(backgroundColor.blue())
                    .arg(backgroundColor.alpha()));
}

void BreakWindow::setTime(int remainingTime, QString estimatedEndTime) {
  // Drive the progress bar from the authoritative remaining time.  Each call
  // starts a short animation from the bar's current value to the new target,
  // giving smooth sub-second interpolation without a long-running animation
  // whose state can desync from the real countdown (e.g. when the animation
  // reaches its end and stops, after which setCurrentTime no longer updates
  // the property).
  int targetValue =
      m_totalSeconds > 0
          ? static_cast<int>(ui->progressBar->maximum() *
                             static_cast<double>(remainingTime) / m_totalSeconds)
          : 0;
  m_progressAnim->stop();
  m_progressAnim->setStartValue(ui->progressBar->value());
  m_progressAnim->setEndValue(targetValue);
  m_progressAnim->setDuration(1000);
  m_progressAnim->start();
  if (m_totalSeconds <= 60) {
    ui->countDownLabel->setText(QString("%1").arg(remainingTime));
  } else {
    ui->countDownLabel->setText(formatTime(remainingTime));
  }
  if (remainingTime > 0) {
    ui->breakEndTimeLabel->setText(tr("Break will end at: %1").arg(estimatedEndTime));
  } else {
    ui->breakEndTimeLabel->setText(tr("Break has ended"));
  }
}

void BreakWindow::setClock(QString hourMinute) { ui->clock->setText(hourMinute); }

void BreakWindow::showButtons(AbstractBreakWindows::Buttons buttons, bool show) {
  // Lock screen is not supported on Flatpak
#ifndef LINUX_DIST_FLATPAK
  if (buttons.testFlag(AbstractBreakWindows::Button::LockScreen))
    ui->lockScreenGroup->setVisible(show);
#endif
  if (buttons.testFlag(AbstractBreakWindows::Button::ExitForceBreak))
    ui->exitForceBreakGroup->setVisible(show);
}

void BreakWindow::showFullScreen() {
  mainWidget->setProperty("isFullScreen", true);
  if (m_supportTransparentInput) {
    // setWindowFlag will reparent the window and do a lot of unnecessary works
    // Instead, we just change the flags of QWindow
    windowHandle()->setFlag(Qt::WindowTransparentForInput, false);
  }
  if (m_bgAnim) m_bgAnim->stop();
  setProperty("color", m_data.theme.mainBackground);

  ui->breakLabel->setText(m_data.message.fullScreen);
  ui->progressBar->setVisible(m_data.show.prograssBar);
  ui->postponeLabel->setVisible(m_data.isPostponed);
  ui->countDownLabel->setVisible(m_data.show.countdown);
  ui->clock->setVisible(m_data.show.clock);
  if (m_data.show.clock) {
    ui->clock->setStyleSheet(
        QString("QLabel { background: transparent; font-size: %1px; }")
            .arg(m_data.show.countdown ? 50 : 100));
  }
  ui->breakEndTimeLabel->setVisible(m_data.show.endTime);
  ui->buttons->setVisible(m_data.show.buttons);

  QPropertyAnimation* resizeAnim =
      new QPropertyAnimation(m_waylandWorkaround ? mainWidget : this, "geometry");
  resizeAnim->setStartValue(m_waylandWorkaround ? mainWidget->geometry() : geometry());
  // Animate to the screen this window was pinned to (see screenIdentity()) so
  // the overlay always follows the display the window was created for, even if
  // Qt migrates the window after a screen change.
  resizeAnim->setEndValue(m_waylandWorkaround ? m_screen->availableGeometry()
                                              : m_screen->geometry());
  resizeAnim->setDuration(300);
  resizeAnim->start();

  if (m_bgImageLabel) {
    m_bgImageLabel->show();
    auto* fadeIn = new QPropertyAnimation(m_bgImageOpacity, "opacity");
    fadeIn->setStartValue(0.0);
    fadeIn->setEndValue(1.0);
    fadeIn->setDuration(800);
    fadeIn->start(QPropertyAnimation::DeleteWhenStopped);
  }
}

void BreakWindow::showFlashPrompt() {
  mainWidget->setProperty("isFullScreen", false);
  if (m_supportTransparentInput) {
    windowHandle()->setFlag(Qt::WindowTransparentForInput, true);
  }
  if (m_bgImageLabel) {
    auto* fadeOut = new QPropertyAnimation(m_bgImageOpacity, "opacity");
    fadeOut->setStartValue(1.0);
    fadeOut->setEndValue(0.0);
    fadeOut->setDuration(100);
    fadeOut->start(QPropertyAnimation::DeleteWhenStopped);
    connect(fadeOut, &QPropertyAnimation::finished, m_bgImageLabel, &QLabel::hide);
  }
  if (m_bgAnim) {
    m_bgAnim->start();
  } else {
    setColor(m_data.theme.mainBackground);
  }

  ui->breakLabel->setText(m_data.message.prompt);
  ui->progressBar->setVisible(m_data.show.prograssBar);
  ui->postponeLabel->setVisible(false);
  ui->countDownLabel->setVisible(false);
  ui->clock->setVisible(false);
  ui->breakEndTimeLabel->setVisible(false);
  ui->buttons->setVisible(false);

  QPropertyAnimation* resizeAnim =
      new QPropertyAnimation(m_waylandWorkaround ? mainWidget : this, "geometry");
  QRect rect =
      m_waylandWorkaround ? m_screen->availableGeometry() : m_screen->geometry();
  QRect targetGeometry = QRect(rect.x() + rect.width() / 2 - SMALL_WINDOW_WIDTH / 2,
                               rect.y(), SMALL_WINDOW_WIDTH, SMALL_WINDOW_HEIGHT);
  resizeAnim->setStartValue(m_waylandWorkaround ? mainWidget->geometry() : geometry());
  resizeAnim->setEndValue(targetGeometry);
  resizeAnim->setDuration(100);
  resizeAnim->start();
}

void BreakWindow::initSize(QScreen* screen) {
  m_screen = screen;
  if (m_waylandWorkaround) {
    QRect rect = screen->availableGeometry();
    // Avoid using full height when initializing the main window. GNOME will refuse to
    // make the window always on top (done in custom shell extension) if it is too large
    // See https://askubuntu.com/questions/1122921.
    rect.setHeight(100);
    setGeometry(rect);
    mainWidget->setGeometry(rect.x() + rect.width() / 2 - SMALL_WINDOW_WIDTH / 2,
                            rect.y(), SMALL_WINDOW_WIDTH, SMALL_WINDOW_HEIGHT);
  } else {
    QRect rect = screen->geometry();
    setGeometry(rect.x() + rect.width() / 2 - SMALL_WINDOW_WIDTH / 2, rect.y(),
                SMALL_WINDOW_WIDTH, SMALL_WINDOW_HEIGHT);
  }
  createWinId();
  // Pin the surface to the requested output. On Wayland + layer-shell this makes
  // LayerShellQt place the surface on the right output (it reads QWindow::screen()
  // by default); on other platforms it keeps screen() authoritative after creation.
  // Must come after createWinId() so windowHandle() is valid, and before show() so
  // the surface is created on the right output from the start.
  if (windowHandle() && screen) windowHandle()->setScreen(screen);
#ifdef Q_OS_MACOS
  macSetAllWorkspaces(windowHandle());
#endif

  if (!m_data.theme.backgroundImage.isEmpty()) {
    QUrl url(m_data.theme.backgroundImage);
    QString path = url.isLocalFile() ? url.toLocalFile() : m_data.theme.backgroundImage;
    QPixmap original(path);
    if (!original.isNull()) {
      QSize targetSize = m_waylandWorkaround ? screen->availableGeometry().size()
                                             : screen->geometry().size();
      QPixmap scaled = original.scaled(targetSize, Qt::KeepAspectRatioByExpanding,
                                       Qt::SmoothTransformation);
      int x = (scaled.width() - targetSize.width()) / 2;
      int y = (scaled.height() - targetSize.height()) / 2;

      m_bgImageLabel = new QLabel(mainWidget);
      m_bgImageLabel->setPixmap(
          scaled.copy(x, y, targetSize.width(), targetSize.height()));
      m_bgImageLabel->setGeometry(0, 0, targetSize.width(), targetSize.height());
      m_bgImageLabel->lower();

      m_bgImageOpacity = new QGraphicsOpacityEffect(m_bgImageLabel);
      m_bgImageOpacity->setOpacity(0);
      m_bgImageLabel->setGraphicsEffect(m_bgImageOpacity);
      m_bgImageLabel->hide();
    }
  }
}

void BreakWindow::colorizeButton(QPushButton* button, QColor color) {
  auto pixmap = button->icon().pixmap(button->iconSize());
  auto mask = pixmap.mask();
  pixmap.fill(color);
  pixmap.setMask(mask);
  button->setIcon(pixmap);
}
