// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include "mainwindow.h"

#include <QRandomGenerator>
#include <QVarLengthArray>

#include <QImage>
#include <QTouchEvent>

#include <cmath>

MainWindow::MainWindow(QRhi::Implementation api) : PainterWindow(api)
{
    setTitle(QStringLiteral("Qt Compact Health"));
    m_ecgGraph.m_mainWindow = this;
    setFillColor(m_theme.background1());

    m_views[W1].title = QStringLiteral("Pulse (BPM)");
    m_views[W2].title = QStringLiteral("SpO2 (%)");
    m_views[W3].title = QStringLiteral("Resp awRR (BRPM)");
    m_views[W4].title = QStringLiteral("Temperature (°C)");
    m_views[Center].fillBackground = false;
    m_views[B3].fillBackground = false;

    initializeTempData();
    initializeRespData();
    m_ecgGraph.initialize();

    connect(&m_timer, &QTimer::timeout, this, &MainWindow::animateData);
    m_timer.start(500);
    m_elapsedTimer.start();

    //m_texts.resize(m_texts.capacity());
}

MainWindow::~MainWindow() { }

void MainWindow::initializeTempData()
{
    auto *r = QRandomGenerator::global();
    m_tempData.resize(m_tempDataCount);
    const float average = 37.0f;
    const float deltaUp = 5.0f;
    const float deltaDown = 2.0f;
    const float range = m_temperatureMax - m_temperatureMin;
    const float varying = 0.04f;
    float d = 0;
    float v = (average + r->generateDouble() * deltaUp - r->generateDouble() * deltaDown
               - m_temperatureMin)
            / range;
    for (int i = 0; i < m_tempDataCount; i++) {
        d = 0.5f * varying - varying * r->generateDouble();
        v += d;
        v = qBound(0.0f, v, 1.0f);
        m_tempData[i] = v;
    }
    // Set the current temperature to be the last measuremet in graph
    m_temperature = m_temperatureMin + m_tempData.last() * range;
    m_dirty.setFlag(Dirty::TemperatureBars);
}
void MainWindow::initializeRespData()
{
    m_respData.fill(-1.0f, m_respDataCount);
}

void MainWindow::updateRespData()
{
    // Currently this graph works so that a single point is
    // added at every update. This means that when the screen
    // update frequency is lower, the area graph "spikes" are
    // shorter and when higher they are longer. But timing is
    // correct, so "breath" takes same time. If output should
    // look similar with all frequencys then [n] amount of points
    // need to be added depending on elapsed time.

    // Update on every other frame
    static int updateTick = 0;
    updateTick++;
    if (updateTick % 2 != 0)
        return;
    // Update m_awrr based on current activity level
    const float awrrAS = 0.002f;
    m_awrr = (1.0f - awrrAS) * m_awrr + awrrAS * (12 + m_activity * 20);

    // Generate data that looks like resp CO2.
    static quint64 prevTickMs = 0;
    quint64 currentMs = m_elapsedTimer.elapsed();
    quint64 elapsedMs = currentMs - prevTickMs;
    prevTickMs = currentMs;
    float elapsedMin = float(elapsedMs) / 60000;
    static float freq = -0.2f;
    freq += m_awrr * elapsedMin;
    auto *r = QRandomGenerator::global();
    static float prevPhase = 0;
    float phase = std::fmod(freq + 0.2f, 1.0f);
    static float fade = 0.2f;
    static float boxy = 6.0f;
    if (phase < prevPhase) {
        // Starting a new phase, randomize
        fade = 0.3f * r->generateDouble();
        boxy = 1.5f + 6.0f * r->generateDouble();
    }
    prevPhase = phase;
    float resp;
    resp = std::sin(float(freq) * M_PI * 2);
    resp = std::tanh(boxy * resp);
    resp += fade * std::sin(float(freq - 0.2f) * M_PI * 2);
    resp = resp * 0.8f - 0.2f;
    resp = qBound(-1.0f, resp, 1.0f);
    m_respData.removeFirst();
    m_respData.append(resp);
}

//![paint-1]
void MainWindow::paint(QCanvasPainter *p)
{
    if (!m_initialized) {
        auto flags = QCanvasPainter::ImageFlag::GenerateMipmaps;
        m_b1ImageLight = p->addImage(QImage(":/images/icon_random_light.png"), flags);
        m_b1ImageDark = p->addImage(QImage(":/images/icon_random_dark.png"), flags);
        m_b2ImageLight = p->addImage(QImage(":/images/icon_theme_light.png"), flags);
        m_b2ImageDark = p->addImage(QImage(":/images/icon_theme_dark.png"), flags);
        m_b3ImageLight = p->addImage(QImage(":/images/icon_settings_light.png"), flags);
        m_b3ImageDark = p->addImage(QImage(":/images/icon_settings_dark.png"), flags);
        m_sImageLight = p->addImage(QImage(":/images/icon_run_light.png"), flags);
        m_sImageDark = p->addImage(QImage(":/images/icon_run_dark.png"), flags);
        m_initialized = true;
    }
    //![paint-1]
    int textIndex = 0;

    m_painter = p;
    // Scalable px
    m_px = float(width()) / 960;
    const float viewRadius = 10 * m_px;
    // Paint view backgrounds
    static QCanvasPath viewBackgroundsPath;
    if (m_dirty.testFlag(Dirty::ViewBackgrounds)) {
        viewBackgroundsPath.clear();
        for (int i = 0; i < ViewsEnd; i++) {
            auto r = m_views[i].rect;
            if (!r.isEmpty() && m_views[i].fillBackground) {
                viewBackgroundsPath.roundRect(r.x(), r.y(), r.width(), r.height(), viewRadius);
            }
        }
    }
    p->setFillStyle(m_theme.background2());
    p->fill(viewBackgroundsPath, StaticPaths);

    if (!m_warningViews.isEmpty()) {
        p->beginPath();
        for (const auto &view : std::as_const(m_warningViews)) {
            auto r = m_views[view].rect;
            p->roundRect(r.x(), r.y(), r.width(), r.height(), viewRadius);
        }
        p->setStrokeStyle(m_theme.warning());
        p->setLineWidth(3.0f * m_px);
        p->stroke();
    }
    // Paint title backgrounds
    static QCanvasPath viewTitlesPath;
    static QVarLengthArray<QRectF> titleRects;
    if (m_dirty.testFlag(Dirty::ViewBackgrounds)) {
        viewTitlesPath.clear();
        titleRects.clear();
        const float titleHeight = m_margin * 4;
        const float titleWidth = m_views[W1].rect.width() - 4 * m_margin;
        const float titleRadius = 0.5 * titleHeight;
        for (int i = W1; i <= W4; i++) {
            auto r = m_views[i].rect;
            if (!r.isEmpty()) {
                QRectF rect(r.x() + 2 * m_margin, r.y() - 0.5 * titleHeight, titleWidth,
                            titleHeight);
                viewTitlesPath.roundRect(rect, titleRadius);
                titleRects << rect;
            }
        }
    }
    p->setFillStyle(m_theme.background1());
    p->setStrokeStyle(m_theme.foreground3());
    p->setLineWidth(3.0f * m_px);
    p->fill(viewTitlesPath, StaticPaths);
    p->stroke(viewTitlesPath, StaticPaths);

    // Widget titles
    p->setFont(m_titleFont);
    p->setFillStyle(m_theme.foreground2());
    p->setTextAlign(QCanvasPainter::TextAlign::Center);
    p->setTextBaseline(QCanvasPainter::TextBaseline::Middle);
    for (int i = 0; i < titleRects.size(); i++) {
        const auto r = titleRects.at(i);
        p->fillText(m_views[i].title, r);
    }

    // Widget texts
    p->setFont(m_bigFont);
    p->setFillStyle(m_theme.foreground1());
    auto r1 = m_views[W1].rect;
    p->fillText(QString::number(int(m_hr)), r1);

    auto r2 = m_views[W2].rect;
    p->fillText(QString::number(m_spo2, 'f', 1), r2);

    auto r3 = m_views[W3].rect;
    p->fillText(QString::number(m_awrr, 'f', 1), r3);

    auto r4 = m_views[W4].rect;
    p->fillText(QString::number(m_temperature, 'f', 1), r4);

    // Paint graphs grid
    const auto gridRect = m_views[Center].rect;
    paintGrid(gridRect.x(), gridRect.y(), gridRect.width(), gridRect.height());

    // Paint graphs
    const auto cRect = m_views[Center].rect;
    const float graphW = cRect.width() - 2 * m_margin;
    const float graphH = (cRect.height() + m_margin) * 0.25f - m_margin;
    const float graphX = cRect.x() + m_margin;
    float graphY = cRect.y() + m_margin;

    // Paint ECG & SpO2 graphs
    m_ecgGraph.updateData(m_hr);
    m_ecgGraph.paintWaves(p, graphX, graphY, graphW, graphH * 2);
    graphY += 2 * graphH + m_margin;

    // Paint resp area graph
    paintRespGraph(graphX, graphY, graphW, graphH);
    graphY += graphH + m_margin;

    // Paint temperature bars graph
    updateRespData();
    paintTempBars(graphX, graphY, graphW, graphH);

    // Settings icon
    QPointF b3C = m_views[B3].rect.center();
    QRectF b3Rect(b3C.x() - 0.5 * m_iconSize, b3C.y() - 0.5 * m_iconSize, m_iconSize, m_iconSize);
    auto &b3Icon = m_theme.isDark() ? m_b3ImageLight : m_b3ImageDark;
    p->setGlobalAlpha(0.3f);
    p->drawImage(b3Icon, b3Rect);
    p->setGlobalAlpha(1.0f);

    // Paint settings view
    if (m_showSettings) {
        const QRectF &sr = m_sliderRect;
        paintSlider(sr.x(), sr.y(), sr.width(), sr.height());
        // Scale images while keeping the aspect ratio.
        QPointF b1C = m_views[B1].rect.center();
        QRectF b1Rect(b1C.x() - 0.5 * m_iconSize, b1C.y() - 0.5 * m_iconSize, m_iconSize,
                      m_iconSize);
        auto &b1Icon = m_theme.isDark() ? m_b1ImageLight : m_b1ImageDark;
        p->drawImage(b1Icon, b1Rect);
        QPointF b2C = m_views[B2].rect.center();
        QRectF b2Rect(b2C.x() - 0.5 * m_iconSize, b2C.y() - 0.5 * m_iconSize, m_iconSize,
                      m_iconSize);
        auto &b2Icon = m_theme.isDark() ? m_b2ImageLight : m_b2ImageDark;
        p->drawImage(b2Icon, b2Rect);
    }
    //![paint-2]
    // Highlight pressed button
    if (m_selectedButton) {
        p->beginPath();
        p->roundRect(m_views[m_selectedButton].rect, viewRadius);
        p->setLineWidth(2.0f * m_px);
        p->setStrokeStyle(m_theme.highlight());
        p->stroke();
    }
    //![paint-2]

    // At this point nothing in backgrounds is dirty
    m_dirty.setFlag(Dirty::ViewBackgrounds, false);

    // Update in VSync
    requestUpdate();
}

void MainWindow::touchEvent(QTouchEvent *ev)
{
    const auto type = ev->type();
    QPointF pos;
    QPointF pressPos;
    if (ev->pointCount() > 0) {
        const auto point = ev->point(0);
        pos = point.position();
        pressPos = point.pressPosition();
    }

    if ((type == QTouchEvent::TouchBegin || type == QTouchEvent::TouchUpdate)
        && m_sliderRect.contains(pressPos)) {
        // Controlling slider
        m_activity = ((m_sliderRect.y() + m_sliderRect.height() - m_sliderMarginBottom - pos.y())
                      / (m_sliderRect.height() - m_sliderMarginTop - m_sliderMarginBottom));
        m_activity = qBound(0.0f, m_activity, 1.0f);
        m_dirty.setFlag(Dirty::ActivitySlider);
    } else if (type == QTouchEvent::TouchBegin) {
        // Pressing button
        for (int i = B1; i <= B3; i++) {
            if (m_views[i].rect.contains(pressPos))
                m_selectedButton = i;
        }
    } else if (type == QTouchEvent::TouchEnd) {
        // Releasing, see if button is being pressed
        if (m_selectedButton)
            buttonClicked(m_selectedButton);
        m_selectedButton = 0;
    } else if (type == QTouchEvent::TouchUpdate) {
        // Moving away from above button
        if (m_selectedButton && !m_views[m_selectedButton].rect.contains(pos))
            m_selectedButton = 0;
    } else if (type == QTouchEvent::TouchCancel) {
        m_selectedButton = 0;
    }

    ev->accept();
}

void MainWindow::buttonClicked(int buttonID)
{
    if (buttonID == B1) {
        initializeTempData();
    } else if (buttonID == B2) {
        m_theme.switchTheme();
        m_ecgGraph.m_theme = &m_theme;
        setFillColor(m_theme.background1());
    } else if (buttonID == B3) {
        m_showSettings = !m_showSettings;
        updateViewSizes();
    }
}

void MainWindow::resizeEvent(QResizeEvent *)
{
    updateViewSizes();
}

void MainWindow::updateViewSizes()
{
    const float w = width();
    const float h = height();
    m_margin = w * 0.01f;
    const float leftColumnWidth = w * 0.22f - m_margin;
    const float buttonSize = w * 0.06f;
    const float rightColumnWidth = m_showSettings ? buttonSize + 2 * m_margin : m_margin;
    m_iconSize = buttonSize * 0.45f;
    const float centerColumnWidth = w - leftColumnWidth - rightColumnWidth - 2 * m_margin;
    const float wMargin = 3.25f * m_margin;
    const float wMargin2 = 3 * m_margin;
    const float wHeight = h * 0.25f - wMargin;
    m_sliderRect.setRect(w * 0.8, m_margin, w * 0.2, h * 0.5 - m_margin);
    m_sliderMarginTop = m_iconSize * 2.0;
    m_sliderMarginBottom = m_iconSize * 0.5;
    m_views[W1].rect = { m_margin, 3 * m_margin, leftColumnWidth, wHeight };
    m_views[W2].rect = { m_margin, m_views[W1].rect.bottom() + wMargin2, leftColumnWidth, wHeight };
    m_views[W3].rect = { m_margin, m_views[W2].rect.bottom() + wMargin2, leftColumnWidth, wHeight };
    m_views[W4].rect = { m_margin, m_views[W3].rect.bottom() + wMargin2, leftColumnWidth, wHeight };
    m_views[Center].rect = { m_views[W1].rect.right() + m_margin, m_margin, centerColumnWidth,
                             h - 2 * m_margin };
    m_views[Slider].rect = { m_views[Center].rect.right() + m_margin, m_margin, buttonSize,
                             h - 2 * buttonSize - 4 * m_margin };
    m_views[B1].rect = { m_views[Center].rect.right() + m_margin,
                         m_views[Slider].rect.bottom() + m_margin, buttonSize, buttonSize };
    m_views[B2].rect = { m_views[Center].rect.right() + m_margin,
                         m_views[B1].rect.bottom() + m_margin, buttonSize, buttonSize };
    m_views[B3].rect = { m_views[Center].rect.right() - buttonSize, m_margin, buttonSize,
                         buttonSize };
    const float m2 = m_margin * 2;
    m_sliderRect = m_views[Slider].rect.adjusted(0, m2, 0, -m2);

    m_titleFont.setPixelSize(wMargin2 * 0.55f);
    m_bigFont.setPixelSize(w * 0.05f);
    // After resize, consider every path to be dirty
    m_dirty.setFlag(Dirty::All);
}

void MainWindow::paintTempBars(float x, float y, float w, float h)
{
    if (m_dirty.testFlag(Dirty::TemperatureBars)) {
        m_tempBarsPath.clear();
        const int rects = m_tempDataCount;
        float margin = (w / rects) * 0.2f;
        float barWidth = w / rects - margin;
        for (int i = 0; i < rects; i++) {
            float posX = x + i * (barWidth + margin);
            float posY = y + h;
            float dh = -h * m_tempData.at(i);
            m_tempBarsPath.rect(posX, posY, barWidth, dh);
        };
        m_dirty.setFlag(Dirty::TemperatureBars, false);
    }
    auto g1 = m_theme.gradient1();
    g1.setStartPosition(0, y);
    g1.setEndPosition(0, y + h);
    m_painter->setFillStyle(g1);
    m_painter->fill(m_tempBarsPath, GraphPaths);
}

void MainWindow::paintRespGraph(float x, float y, float w, float h)
{
    m_painter->beginPath();
    float firstPosY = y + 0.5 * h - m_respData.constFirst() * 0.5 * h;
    m_painter->moveTo(x, firstPosY);
    for (int i = 1; i < m_respDataCount; i++) {
        const float fi = float(i);
        const float resp = m_respData.at(i);
        float posX = x + (fi / m_respDataCount) * w;
        float posY = y + 0.5 * h - resp * 0.5 * h;
        m_painter->lineTo(posX, posY);
    }
    m_painter->lineTo(x + w, y + h);
    m_painter->lineTo(x, y + h);
    m_painter->closePath();
    auto g2 = m_theme.gradient2();
    g2.setStartPosition(0, y);
    g2.setEndPosition(0, y + h);
    m_painter->setFillStyle(g2);
    m_painter->fill();
}

void MainWindow::paintSlider(float x, float y, float w, float h)
{
    static QCanvasPath sliderBackground;
    static QCanvasPath sliderFill;
    static QCanvasPath sliderKnob;
    QRectF sRect(x + 0.5 * w - 0.5 * m_iconSize, y, m_iconSize, m_iconSize);
    auto &sIcon = m_theme.isDark() ? m_sImageLight : m_sImageDark;
    m_painter->drawImage(sIcon, sRect);
    if (m_dirty.testFlag(Dirty::ActivitySlider)) {
        sliderBackground.clear();
        const float barW = 4.0f;
        const float knobSize = 16.0f;
        const float radius = barW * 0.5f;
        const float cW = x + w * 0.5;
        const float margins = m_sliderMarginTop + m_sliderMarginBottom;
        const float barY = y + m_sliderMarginTop + (h - margins) * (1.0f - m_activity);
        sliderBackground.roundRect(cW - barW * 0.5, y + m_sliderMarginTop, barW, h - margins,
                                   radius);
        sliderFill.clear();
        sliderFill.roundRect(cW - barW * 0.5, barY, barW, (h - margins) * m_activity, radius);
        sliderKnob.clear();
        sliderKnob.circle(cW, barY, knobSize);
        m_dirty.setFlag(Dirty::ActivitySlider, false);
    }
    m_painter->setFillStyle(m_theme.foreground3());
    m_painter->fill(sliderBackground, ActivitySliderPaths);
    m_painter->setFillStyle(m_theme.highlight());
    m_painter->fill(sliderFill, ActivitySliderPaths);
    m_painter->setFillStyle(m_theme.foreground3());
    m_painter->setStrokeStyle(m_theme.foreground2());
    m_painter->setLineWidth(2.0f);
    m_painter->fill(sliderKnob, ActivitySliderPaths);
    m_painter->stroke(sliderKnob, ActivitySliderPaths);
}

void MainWindow::paintGrid(float x, float y, float w, float h)
{
    const int hLines = 40;
    const int vLines = 20;
    auto *p = m_painter;
    static QCanvasPath path;
    if (m_dirty.testFlag(Dirty::GraphGrid)) {
        path.clear();
        // Hozontal minor lines
        for (int i = 0; i <= hLines; i++) {
            float yPos = y + float(i) / hLines * h;
            path.moveTo(x, yPos);
            path.lineTo(x + w, yPos);
        }

        // Vertical minor lines
        p->setLineWidth(1.0);
        for (int i = 0; i <= vLines; i++) {
            float xPos = x + float(i) / vLines * w;
            path.moveTo(xPos, y);
            path.lineTo(xPos, y + h);
        }
        m_dirty.setFlag(Dirty::GraphGrid, false);
    }
    p->setStrokeStyle(m_theme.foreground3());
    p->setLineWidth(1.0f);
    p->stroke(path, StaticPaths);
}

void MainWindow::animateData()
{
    auto *r = QRandomGenerator::global();
    quint64 elapsed = m_elapsedTimer.elapsed();

    // HR adjustment speed
    const float hrAS = 0.02f;
    m_hr = (1.0f - hrAS) * m_hr + hrAS * (40 + 170 * m_activity);
    const float hrRand = 0.005f;
    m_hr *= (1.0f + hrRand - 2 * hrRand * r->generateDouble());

    const float spRand = 0.0005f;
    m_spo2 *= (1.0f + spRand - 2 * spRand * r->generateDouble());
    m_spo2 += spRand;
    m_spo2 = qBound(0.0f, m_spo2, 100.0f);

    const float tempAS = 0.02f;
    m_temperature = (1.0f - tempAS) * m_temperature + tempAS * (35.5f + 1.8f * (m_hr * 0.01f));
    const float tempRand = 0.0005f;
    m_temperature *= (1.0f + tempRand - 2 * tempRand * r->generateDouble());
    static quint64 tempTickMs = 0;
    if (elapsed > tempTickMs + m_temperatureLogMs) {
        // Add temperature into graph
        m_tempData.removeFirst();
        float t = (m_temperature - m_temperatureMin) / (m_temperatureMax - m_temperatureMin);
        t = qBound(0.0f, t, 1.0f);
        m_tempData.append(t);
        tempTickMs = elapsed;
        m_dirty.setFlag(Dirty::TemperatureBars);
    }
    updateWarnings();
}

void MainWindow::updateWarnings()
{
    if (m_temperature >= 38 || m_temperature < 36)
        m_warningViews << W4;
    else
        m_warningViews.remove(W4);

    if (m_hr > 180 || m_hr < 45)
        m_warningViews << W1;
    else
        m_warningViews.remove(W1);

    if (m_spo2 < 96.0f)
        m_warningViews << W2;
    else
        m_warningViews.remove(W2);
};
