// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#pragma once

#include "profiler_global.h"

#include <utils/aspects.h>
#include <utils/commandline.h>
#include <utils/environment.h>
#include <utils/filepath.h>
#include <utils/id.h>
#include <utils/result.h>

#include <QtTaskTree/QTaskTree>

#include <QMutex>
#include <QPointer>
#include <QString>
#include <QUrl>

#include <algorithm>
#include <atomic>
#include <chrono>
#include <functional>
#include <memory>
#include <optional>
#include <utility>
#include <vector>

namespace Profiler::Internal {

// Options for a single sampling session. The target is selected by `pid` when it
// is non-zero (e.g. a process launched to be profiled), otherwise by `processName`.
struct SamplerOptions
{
    qint64 pid = 0;       // Process id to attach to; 0 selects by processName instead.
    QString processName;  // Executable basename to attach to, e.g. "Qt Creator".
    int intervalUs = 200; // Target delay between samples; 0 = as fast as possible.
};

// Announces that what a frontend shows of a recording has changed. Its own
// QObject, because a capture working on a thread of its own reports through it
// and Qt's queued delivery is what makes that safe -- the session itself stays
// a plain struct that any thread may hold.
class PROFILER_EXPORT RecordingReporter : public QObject
{
    Q_OBJECT

signals:
    void changed();
};

// Shared state for one recording session. The GUI thread owns it and observes
// progress while a worker thread records; the sampler task captures it. Always
// handled through a shared_ptr, never copied.
struct RecordingSession : std::enable_shared_from_this<RecordingSession>
{
    // Inputs, set before recording starts.
    int intervalUs = 200;            // Backend-specific cadence hint.
    QString processName;             // Attach-by-name target, used when pid == 0.

    // QmlDebug::ProfileFeature bitmask to record for protocol-based backends; 0
    // means "record everything". Carried on the session so a composite backend
    // can drive its QML sub-capture (see CombinedSampler).
    quint64 requestedFeatures = 0;

    // Set when a target is to be launched (otherwise the backend attaches to or
    // connects to an already-running target). The backend composes the launch
    // itself (see launchThenCapture()), so it may rewrite the command, e.g. to
    // inject -qmljsdebugger arguments.
    std::optional<Utils::CommandLine> launchCommand;
    Utils::FilePath launchWorkingDir;
    // The environment to launch in; empty means the one this process inherited.
    Utils::Environment launchEnvironment;

    // QML debug channel for protocol-based backends; empty for native ones.
    QUrl serverUrl;

    // Set once a launched process is running; selects attach-by-pid instead.
    std::atomic<qint64> pid = 0;

    // Runtime control and output.
    std::atomic<int> progress = 0;   // 0..100 post-processing percent.
    std::optional<Utils::Result<Utils::FilePath>> result; // Set on the GUI thread when done.

    // The debug-information download a backend is waiting on, if any. Post-
    // processing can sit on one for minutes, and the plain progress bar cannot
    // move meanwhile, so the recording page says what is going on instead of
    // looking hung (see PerfSampler::captureRecipe()).
    struct DebugInfoDownload
    {
        int percent = -1; // 0..100; 0 when the size is not known yet, -1 when idle.
        QString url;      // The server being queried, empty when not reported.
    };

    void setDebugInfoDownload(int percent, const QString &url)
    {
        {
            QMutexLocker lock(&m_debugInfoDownloadMutex);
            if (m_debugInfoDownload.percent == percent && m_debugInfoDownload.url == url)
                return;
            m_debugInfoDownload = {percent, url};
        }
        notifyReports();
    }

    // Called for every message that is not a download report, so it has to be
    // cheap and quiet when there is nothing to clear.
    void clearDebugInfoDownload()
    {
        {
            QMutexLocker lock(&m_debugInfoDownloadMutex);
            if (m_debugInfoDownload.percent < 0)
                return;
            m_debugInfoDownload = {};
        }
        notifyReports();
    }

    DebugInfoDownload debugInfoDownload() const
    {
        for (const std::shared_ptr<RecordingSession> &child : m_children) {
            const DebugInfoDownload download = child->debugInfoDownload();
            if (download.percent >= 0)
                return download;
        }
        QMutexLocker lock(&m_debugInfoDownloadMutex);
        return m_debugInfoDownload;
    }

    // A composite backend captures through one sub-session per side (see
    // CombinedSampler). Registering them here is what lets one stop request
    // reach every capture, and lets the parent answer for the whole recording
    // when the GUI asks -- both without a timer copying state across.
    //
    // Called while the recipe is built, before any capture runs, and only from
    // the GUI thread, as every stop request is.
    void addSubSession(const std::shared_ptr<RecordingSession> &child)
    {
        // Weak, and held by the child rather than looked up: the parent already
        // owns the child, and a capture still running may outlive either.
        child->m_parent = weak_from_this();
        m_children.push_back(child);
    }

    // Ends the recording: the captures wind down and write what they have.
    // Cascades, so stopping a composite stops both its sides, and one side
    // finishing can end the other.
    void requestStop()
    {
        m_stopRequested = true;
        // Taken by value and cleared first: stopping is a one-shot, and a
        // handler may start something that registers another.
        const std::vector<std::pair<QPointer<QObject>, std::function<void()>>> handlers
            = std::exchange(m_stopHandlers, {});
        for (const auto &[context, handler] : handlers) {
            if (context)
                handler();
        }
        for (const std::shared_ptr<RecordingSession> &child : m_children)
            child->requestStop();
    }

    // Runs `handler` when the recording is asked to stop: a capture driven by
    // events hooks this to wind itself down, and one that samples in a loop of
    // its own hooks it to cancel the task's promise, which is what that loop
    // watches. As with a Qt connection, `context` bounds the handler's life.
    //
    // Registration and every stop request happen on the GUI thread.
    void onStopRequested(QObject *context, const std::function<void()> &handler)
    {
        if (m_stopRequested) {
            // Already asked. Queued rather than called here, because the caller
            // is still building the task this belongs to -- a timer would not
            // have fired until after that either.
            QMetaObject::invokeMethod(context, handler, Qt::QueuedConnection);
            return;
        }
        m_stopHandlers.emplace_back(context, handler);
    }

    // Whether capture is live. A composite is only live once both its sides
    // are: until then a timed recording's clock must not start.
    bool isStarted() const
    {
        if (m_children.empty())
            return m_started.load(std::memory_order_relaxed);
        return std::all_of(m_children.cbegin(), m_children.cend(),
                           [](const std::shared_ptr<RecordingSession> &child) {
                               return child->isStarted();
                           });
    }

    // 0..100 for the progress bar. A composite sets this to say how its sides'
    // progress adds up to the whole, which only it knows: the captures are one
    // phase of it, whatever it does with their results is another.
    std::function<int()> progressProvider;

    int progressPercent() const
    {
        return progressProvider ? progressProvider() : progress.load(std::memory_order_relaxed);
    }

    void setProgress(int percent)
    {
        if (progress.exchange(percent, std::memory_order_relaxed) != percent)
            notifyReports();
    }

    // Emits changed() whenever what a frontend shows of this recording moves:
    // progress, the debug-info download, or capture going live. Connect to it
    // instead of reading those on a timer.
    RecordingReporter *reporter() const { return m_reporter.get(); }

    // Announces such a change. Safe from any thread -- the emission is queued
    // onto the reporter's own, which is the GUI thread -- so a capture may
    // report from wherever it happens to work.
    void notifyReports()
    {
        QMetaObject::invokeMethod(m_reporter.get(),
                                  [reporter = m_reporter] { emit reporter->changed(); },
                                  Qt::QueuedConnection);
        // A sub-session's report is the whole recording's, so it carries up to
        // whoever is watching the parent.
        if (const std::shared_ptr<RecordingSession> parent = m_parent.lock())
            parent->notifyReports();
    }

    // Monotonic instant (steady_clock, microseconds) at which the backend went
    // live, or -1 if it never did. steady_clock is a single process-wide timeline,
    // so two sessions recorded together are directly comparable -- this is what
    // lets a combined recording correlate its two traces onto one axis.
    std::atomic<qint64> startedMonotonicUs = -1;

    // Marks the capture live, stamping the monotonic instant on the first call
    // (later calls only re-set the flag). Backends call this at the exact point
    // they begin capturing, in place of setting `started` directly.
    void markStarted()
    {
        if (!m_started.exchange(true)) {
            startedMonotonicUs.store(std::chrono::duration_cast<std::chrono::microseconds>(
                                         std::chrono::steady_clock::now().time_since_epoch())
                                         .count(),
                                     std::memory_order_relaxed);
            notifyReports();
        }
    }

private:
    // The only state here that is not a lone integer, so it cannot ride on an
    // atomic like the rest: a backend may report from the thread it processes on
    // while the GUI polls, and the percentage and the URL have to agree.
    mutable QMutex m_debugInfoDownloadMutex;
    DebugInfoDownload m_debugInfoDownload;

    // Written by markStarted() and read through isStarted(), which is what a
    // composite has to answer for its sub-sessions rather than for itself.
    std::atomic_bool m_started = false;

    // Only so a handler registered after the fact still runs. Nothing polls a
    // stop any more: a capture with a loop of its own watches its task's
    // promise, everyone else is called (see requestStop()).
    bool m_stopRequested = false;

    std::vector<std::shared_ptr<RecordingSession>> m_children;
    std::weak_ptr<RecordingSession> m_parent;
    std::vector<std::pair<QPointer<QObject>, std::function<void()>>> m_stopHandlers;

    // Deleted through deleteLater(), so that a worker thread dropping the last
    // reference to the session does not destroy a QObject off its own thread.
    std::shared_ptr<RecordingReporter> m_reporter{new RecordingReporter,
                                                  [](RecordingReporter *r) { r->deleteLater(); }};
};

// Backend-specific recording settings. Besides holding the options, it renders its
// own configuration controls via AspectContainer::setLayouter(), keeping them next
// to the settings they use. All backends can launch an executable, so the launch
// command lives here; backend-specific alternatives (attach to a pid, connect to a
// debug server) are added by subclasses, which decide in createSession() which
// mode the current settings select.
class PROFILER_EXPORT SamplerSettings : public Utils::AspectContainer
{
    Q_OBJECT

public:
    SamplerSettings();

    // The executable to launch and profile, used unless a subclass selects a
    // different start mode (attach/connect).
    Utils::FilePathAspect executable{this};
    Utils::StringAspect arguments{this};
    Utils::FilePathAspect workingDirectory{this};

    // What the frontend last put into the three fields above (see
    // ProfilerRecorder::seedLaunchTarget). Persisted, so a value the user typed
    // over a seeded one still reads as theirs after a restart. Never shown.
    Utils::FilePathAspect seededExecutable{this};
    Utils::StringAspect seededArguments{this};
    Utils::FilePathAspect seededWorkingDirectory{this};

    // Builds a RecordingSession from the current settings, or an error explaining
    // what is missing/misconfigured (e.g. no executable, or attach with no process).
    virtual Utils::Result<std::shared_ptr<RecordingSession>> createSession() const = 0;

    // Builds a RecordingSession for a target something else launches -- Qt
    // Creator's run machinery, say (see profilersamplerruncontrol.cpp). It
    // carries the backend's own options and no target of its own, so it cannot
    // be missing one and never fails.
    std::shared_ptr<RecordingSession> createRunControlSession() const;

    // Set while a target is chosen elsewhere. The settings that pick one -- the
    // launch fields, and any attach or connect alternative -- then go read-only
    // rather than away, so the backend's remaining options stay usable.
    void setTargetChosenElsewhere(bool chosen);
    bool targetChosenElsewhere() const { return m_targetChosenElsewhere; }

signals:
    // Emitted when setTargetChosenElsewhere() changes what may be picked, for
    // the controls that are not aspects (a "Select Process..." button, say) and
    // that updateTargetEnabled() therefore cannot reach.
    void targetSelectionChanged();

protected:
    // Populates session->launchCommand/launchWorkingDir from executable+arguments;
    // fails if no executable is set.
    Utils::Result<> fillLaunch(RecordingSession &session) const;

    // Copies the backend's own options -- a sampling cadence, the features to
    // record -- onto `session`, leaving its target alone. Both ways of building
    // a session go through this, so the two cannot drift apart.
    virtual void fillOptions(RecordingSession &session) const;

    // Applies targetChosenElsewhere() to the settings that pick a target. The
    // base handles the launch fields; a backend offering an attach or connect
    // alternative extends this.
    virtual void updateTargetEnabled();

private:
    bool m_targetChosenElsewhere = false;
};

// A repair for a system setting that stops a backend from recording, offered to
// the user as a button next to the failure. The command is run as root, so it
// names one specific change and nothing else.
struct SamplerFix
{
    QString buttonText;         // Button label, e.g. "Allow Sampling".
    QString detail;             // What applying it does, shown with the error.
    Utils::CommandLine command; // Run with root privileges by the UI.
};

// A profiling backend: records a trace of a target process until the session is
// stopped, then writes it and reports the resulting trace directory.
//
// Implementations differ in how they capture (call-stack sampling today; a QML
// profiler or perf-based recorder could be added alongside). Each returns a
// QtTaskTree recipe so the window can compose it with process launching.
// Identifies a backend to a frontend that has its own way of profiling the same
// thing, and does not change when the display name is translated.
namespace SamplerIds {
inline constexpr char CallStack[] = "Profiler.Sampler.CallStack";
inline constexpr char Perf[]      = "Profiler.Sampler.Perf";
inline constexpr char Qml[]       = "Profiler.Sampler.Qml";
inline constexpr char Combined[]  = "Profiler.Sampler.Combined";
} // namespace SamplerIds

class PROFILER_EXPORT Sampler
{
public:
    virtual ~Sampler() = default;

    virtual Utils::Id id() const = 0;
    virtual QString displayName() const = 0;

    // Whether this backend can run in the current environment; when it cannot,
    // fills *error with a human-readable reason.
    virtual bool isAvailable(QString *error = nullptr) const = 0;

    // The complete recipe that records the target described by `session`: it
    // prepares and launches session->launchCommand (when set) and captures the
    // target until a stop is requested, storing its Result into session->result
    // when done. Concrete: it calls prepareLaunch() and then wraps captureRecipe() in
    // launchThenCapture(), so the caller never needs to know how a particular
    // backend starts its target. Backends customise the two hooks below.
    QtTaskTree::ExecutableItem recordRecipe(
        const std::shared_ptr<RecordingSession> &session) const;

    // Adjusts session->launchCommand before the target is launched -- e.g. the
    // QML backend injects -qmljsdebugger and allocates session->serverUrl here.
    // Called by recordRecipe() before the process starts; the default does
    // nothing. Public so a composite backend (see CombinedSampler) can delegate
    // to the backends it wraps.
    virtual void prepareLaunch(const std::shared_ptr<RecordingSession> &session) const;

    // The capture portion of the recipe, run after the target (if any) is
    // launched. Reads session->pid / session->serverUrl and
    // stores the trace path into session->result. recordRecipe() wraps this in
    // launchThenCapture(); a composite backend calls it directly for each
    // backend it wraps.
    virtual QtTaskTree::ExecutableItem captureRecipe(
        const std::shared_ptr<RecordingSession> &session) const = 0;

    // Whether the target has to be started with a QML debug server for this
    // backend to capture it. prepareLaunch() arranges that for a launch the
    // backend composes itself; a frontend launching through Qt Creator's run
    // machinery has to arrange it on the run control instead, and asks this.
    virtual bool needsQmlChannel() const { return false; }

    // Backend-specific recording settings, which also render the backend's own
    // configuration controls and attach/connect start buttons (see SamplerSettings).
    // Null when the backend has no options. Owned by the backend.
    virtual SamplerSettings *settings() const { return nullptr; }

    // A system setting the user can change to make recording work, queried after
    // a recording failed. Re-reads the setting rather than interpreting the error,
    // so it only offers a change that is still needed. None by default.
    virtual std::optional<SamplerFix> availableFix() const { return std::nullopt; }
};

} // namespace Profiler::Internal
