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

#include "simulatorcontrol.h"
#include "iosconfigurations.h"
#include "iostr.h"

#include <utils/algorithm.h>
#include <utils/async.h>
#include <utils/qtcprocess.h>
#include <utils/qtcassert.h>

#ifdef Q_OS_MAC
#include <CoreFoundation/CoreFoundation.h>
#endif

#include <memory>

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLoggingCategory>

using namespace Utils;

using namespace std;
using namespace std::chrono;

namespace {
static Q_LOGGING_CATEGORY(simulatorLog, "qtc.ios.simulator", QtWarningMsg)
}

namespace Ios::Internal {

const seconds simulatorStartTimeout = seconds(60);

// simctl Json Tags and tokens.
const char devicesTag[] = "devices";
const char availabilityTag[] = "availability";
const char unavailabilityToken[] = "unavailable";
const char availabilityTagNew[] = "isAvailable"; // at least since Xcode 10
const char nameTag[] = "name";
const char stateTag[] = "state";
const char udidTag[] = "udid";

static Result<> runCommand(
    const CommandLine &command,
    QString *stdOutput,
    std::function<bool()> shouldStop = [] { return false; })
{
    Process p;
    p.setCommand(command);
    p.start();

    if (!p.waitForStarted())
        return make_unexpected(Tr::tr("Failed to start process."));

    forever {
        if (shouldStop() || p.waitForFinished(seconds(1)))
            break;
    }

    if (p.state() != QProcess::ProcessState::NotRunning) {
        p.kill();
        if (shouldStop())
            return make_unexpected(Tr::tr("Process was canceled."));

        return make_unexpected(Tr::tr("Process was forced to exit."));
    }

    if (stdOutput)
        *stdOutput = p.cleanedStdOut();

    if (p.result() != ProcessResult::FinishedWithSuccess) {
        QStringList error;
        if (const QString procError = p.errorString(); !procError.isEmpty())
            error << procError;
        if (const QString stdError = p.cleanedStdErr(); !stdError.isEmpty())
            error << stdError;
        return make_unexpected(error.join('\n'));
    }

    return {};
}

static Result<> runSimCtlCommand(
    const QStringList &args,
    QString *output,
    std::function<bool()> shouldStop = [] { return false; })
{
    // Cache xcrun's path, as this function will be called often.
    static FilePath xcrun = FilePath::fromString("xcrun").searchInPath();
    if (xcrun.isEmpty())
        return make_unexpected(Tr::tr("Cannot find xcrun."));
    else if (!xcrun.isExecutableFile())
        return make_unexpected(Tr::tr("xcrun is not executable."));
    return runCommand({xcrun, {"simctl", args}}, output, shouldStop);
}

static Result<> launchSimulator(const QString &simUdid, std::function<bool()> shouldStop)
{
    QTC_ASSERT(!simUdid.isEmpty(), return make_unexpected(Tr::tr("Invalid Empty UDID.")));
    const FilePath simulatorAppPath = IosConfigurations::developerPath()
            .pathAppended("Applications/Simulator.app/Contents/MacOS/Simulator");

    // boot the requested simulator device
    const Result<> bootResult = runSimCtlCommand({"boot", simUdid}, nullptr, shouldStop);
    if (!bootResult)
        return bootResult;

    // open Simulator.app if not running yet
    return runCommand(
        {"/usr/bin/open",
         {IosConfigurations::developerPath().pathAppended("Applications/Simulator.app").nativePath()}},
        nullptr);
}

static bool isAvailable(const QJsonObject &object)
{
    return object.contains(availabilityTagNew)
               ? object.value(availabilityTagNew).toBool()
               : !object.value(availabilityTag).toString().contains(unavailabilityToken);
}

static SimulatorInfo deviceInfo(const QString &simUdid);
static QString bundleIdentifier(const Utils::FilePath &bundlePath);

static void startSimulator(QPromise<SimulatorControl::Response> &promise, const QString &simUdid);
static void installApp(QPromise<SimulatorControl::Response> &promise,
                       const QString &simUdid,
                       const Utils::FilePath &bundlePath);
static void launchApp(QPromise<SimulatorControl::Response> &promise,
                      const QString &simUdid,
                      const QString &bundleIdentifier,
                      bool waitForDebugger,
                      const QStringList &extraArgs,
                      const QString &stdoutPath,
                      const QString &stderrPath);

static QList<SimulatorInfo> s_availableDevices;

QList<SimulatorInfo> SimulatorControl::availableSimulators()
{
    return s_availableDevices;
}

static QList<SimulatorInfo> getAllSimulatorDevices()
{
    QList<SimulatorInfo> simulatorDevices;
    QString output;
    runSimCtlCommand({"list", "-j", devicesTag}, &output);
    QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8());
    if (!doc.isNull()) {
        const QJsonObject runtimeObject = doc.object().value(devicesTag).toObject();
        const QStringList keys = runtimeObject.keys();
        for (const QString &runtime : keys) {
            const QJsonArray devices = runtimeObject.value(runtime).toArray();
            for (const QJsonValue deviceValue : devices) {
                QJsonObject deviceObject = deviceValue.toObject();
                SimulatorInfo device;
                device.identifier = deviceObject.value(udidTag).toString();
                device.name = deviceObject.value(nameTag).toString();
                device.runtimeName = runtime;
                device.available = isAvailable(deviceObject);
                device.state = deviceObject.value(stateTag).toString();
                simulatorDevices.append(device);
            }
        }
        stable_sort(simulatorDevices.begin(), simulatorDevices.end());
    } else {
        qCDebug(simulatorLog) << "Error parsing json output from simctl. Output:" << output;
    }
    return simulatorDevices;
}

static QList<SimulatorInfo> getAvailableSimulators()
{
    auto filterSim = [](const SimulatorInfo &device) { return device.available;};
    QList<SimulatorInfo> availableDevices = Utils::filtered(getAllSimulatorDevices(), filterSim);
    return availableDevices;
}

QFuture<QList<SimulatorInfo>> SimulatorControl::updateAvailableSimulators(QObject *context)
{
    QFuture<QList<SimulatorInfo>> future = Utils::asyncRun(getAvailableSimulators);
    Utils::onResultReady(future, context, [](const QList<SimulatorInfo> &devices) {
        s_availableDevices = devices;
    });
    return future;
}

bool SimulatorControl::isSimulatorRunning(const QString &simUdid)
{
    if (simUdid.isEmpty())
        return false;
    return deviceInfo(simUdid).isBooted();
}

QString SimulatorControl::bundleIdentifier(const Utils::FilePath &bundlePath)
{
    return Internal::bundleIdentifier(bundlePath);
}

QFuture<SimulatorControl::Response> SimulatorControl::startSimulator(const QString &simUdid)
{
    return Utils::asyncRun(Internal::startSimulator, simUdid);
}

QFuture<SimulatorControl::Response> SimulatorControl::installApp(const QString &simUdid,
                                                                 const Utils::FilePath &bundlePath)
{
    return Utils::asyncRun(Internal::installApp, simUdid, bundlePath);
}

QFuture<SimulatorControl::Response> SimulatorControl::launchApp(const QString &simUdid,
                                                                const QString &bundleIdentifier,
                                                                bool waitForDebugger,
                                                                const QStringList &extraArgs,
                                                                const QString &stdoutPath,
                                                                const QString &stderrPath)
{
    return Utils::asyncRun(Internal::launchApp,
                           simUdid,
                           bundleIdentifier,
                           waitForDebugger,
                           extraArgs,
                           stdoutPath,
                           stderrPath);
}

// Static members

SimulatorInfo deviceInfo(const QString &simUdid)
{
    auto matchDevice = [simUdid](const SimulatorInfo &device) {
        return device.identifier == simUdid;
    };
    SimulatorInfo device = Utils::findOrDefault(getAllSimulatorDevices(), matchDevice);
    if (device.identifier.isEmpty())
        qCDebug(simulatorLog) << "Cannot find device info. Invalid UDID.";

    return device;
}

QString bundleIdentifier(const Utils::FilePath &bundlePath)
{
    QString bundleID;
#ifdef Q_OS_MAC
    if (bundlePath.exists()) {
        CFStringRef cFBundlePath = bundlePath.toUrlishString().toCFString();
        CFURLRef bundle_url = CFURLCreateWithFileSystemPath (kCFAllocatorDefault, cFBundlePath, kCFURLPOSIXPathStyle, true);
        CFRelease(cFBundlePath);
        CFBundleRef bundle = CFBundleCreate (kCFAllocatorDefault, bundle_url);
        CFRelease(bundle_url);
        CFStringRef cFBundleID = CFBundleGetIdentifier(bundle);
        bundleID = QString::fromCFString(cFBundleID).trimmed();
        CFRelease(bundle);
    }
#else
    Q_UNUSED(bundlePath)
#endif
    return bundleID;
}

void startSimulator(QPromise<SimulatorControl::Response> &promise, const QString &simUdid)
{
    SimulatorControl::ResponseData response(simUdid);
    SimulatorInfo simInfo = deviceInfo(simUdid);

    if (!simInfo.available) {
        promise.addResult(
            make_unexpected(Tr::tr("Simulator device is not available. (%1)").arg(simUdid)));
        return;
    }
    // Shutting down state checks are for the case when simulator start is called within a short
    // interval of closing the previous interval of the simulator. We wait untill the shutdown
    // process is complete.
    QDeadlineTimer simulatorStartDeadline(simulatorStartTimeout);
    while (simInfo.isShuttingDown() && !simulatorStartDeadline.hasExpired()) {
        // Wait till the simulator shuts down, if doing so.
        if (promise.isCanceled()) {
            promise.addResult(make_unexpected(Tr::tr("Simulator start was canceled.")));
            return;
        }

        QThread::msleep(100);
        simInfo = deviceInfo(simUdid);
    }

    if (simInfo.isShuttingDown()) {
        promise.addResult(
            make_unexpected(Tr::tr("Cannot start Simulator device. Previous instance taking "
                                   "too long to shut down. (%1)")
                                .arg(simInfo.toString())));
        return;
    }

    if (!simInfo.isShutdown()) {
        promise.addResult(make_unexpected(
            Tr::tr("Cannot start Simulator device. Simulator not in shutdown state. (%1)")
                .arg(simInfo.toString())));
        return;
    }

    Result<> result = launchSimulator(simUdid,
                                                [&promise] { return promise.isCanceled(); });
    if (!result) {
        promise.addResult(make_unexpected(result.error()));
        return;
    }

    // At this point the sim device exists, available and was not running.
    // So the simulator is started and we'll wait for it to reach to a state
    // where we can interact with it.
    // We restart the deadline to give it some more time.
    simulatorStartDeadline = QDeadlineTimer(simulatorStartTimeout);
    SimulatorInfo info;
    do {
        info = deviceInfo(simUdid);
        if (promise.isCanceled()) {
            promise.addResult(make_unexpected(Tr::tr("Simulator start was canceled.")));
            return;
        }
    } while (!info.isBooted() && !simulatorStartDeadline.hasExpired());

    if (!info.isBooted()) {
        promise.addResult(make_unexpected(
            Tr::tr("Cannot start Simulator device. Simulator not in booted state. (%1)")
                .arg(info.toString())));
        return;
    }

    promise.addResult(response);
}

void installApp(QPromise<SimulatorControl::Response> &promise,
                const QString &simUdid,
                const Utils::FilePath &bundlePath)
{
    SimulatorControl::ResponseData response(simUdid);

    if (!bundlePath.exists()) {
        promise.addResult(make_unexpected(Tr::tr("Bundle path does not exist.")));
        return;
    }

    Result<> result
        = runSimCtlCommand({"install", simUdid, bundlePath.toUrlishString()}, nullptr, [&promise] {
              return promise.isCanceled();
          });
    if (!result) {
        promise.addResult(make_unexpected(result.error()));
    } else {
        promise.addResult(response);
    }
}

void launchApp(QPromise<SimulatorControl::Response> &promise,
               const QString &simUdid,
               const QString &bundleIdentifier,
               bool waitForDebugger,
               const QStringList &extraArgs,
               const QString &stdoutPath,
               const QString &stderrPath)
{
    SimulatorControl::ResponseData response(simUdid);

    if (bundleIdentifier.isEmpty()) {
        promise.addResult(make_unexpected(Tr::tr("Invalid (empty) bundle identifier.")));
        return;
    }

    QStringList args({"launch", simUdid, bundleIdentifier});

    // simctl usage documentation : Note: Log output is often directed to stderr, not stdout.
    if (!stdoutPath.isEmpty())
        args.insert(1, QString("--stderr=%1").arg(stdoutPath));

    if (!stderrPath.isEmpty())
        args.insert(1, QString("--stdout=%1").arg(stderrPath));

    if (waitForDebugger)
        args.insert(1, "-w");

    for (const QString &extraArgument : extraArgs) {
        if (!extraArgument.trimmed().isEmpty())
            args << extraArgument;
    }

    QString stdOutput;
    Result<> result = runSimCtlCommand(args, &stdOutput, [&promise] {
        return promise.isCanceled();
    });

    if (!result) {
        promise.addResult(make_unexpected(result.error()));
        return;
    }

    const QString pIdStr = stdOutput.trimmed().split(' ').last().trimmed();
    bool validPid = false;
    response.inferiorPid = pIdStr.toLongLong(&validPid);

    if (!validPid) {
        promise.addResult(make_unexpected(
            Tr::tr("Failed to parse the inferior PID from simctl output (%1).").arg(pIdStr)));
        return;
    }

    promise.addResult(response);
}

QString SimulatorInfo::toString() const
{
    return QString("Name: %1 UDID: %2 Availability: %3 State: %4 Runtime: %5")
        .arg(name)
        .arg(identifier)
        .arg(available)
        .arg(state)
        .arg(runtimeName);
}

bool SimulatorInfo::operator==(const SimulatorInfo &other) const
{
    return identifier == other.identifier
            && state == other.state
            && name == other.name
            && available == other.available
            && runtimeName == other.runtimeName;
}

} // Ios::Internal
