/*
 * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "result.h"
#include "utility.h"

#include <QCoreApplication>
#include <QDir>
#include <QLoggingCategory>

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QtMacExtras/QtMacExtras>
#endif

#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSUserDefaults.h>

#include <sys/sysctl.h>

namespace OCC {

void Utility::setupFavLink(const QString &folder)
{
    // Finder: Place under "Places"/"Favorites" on the left sidebar
    CFStringRef folderCFStr = CFStringCreateWithCString(nullptr, folder.toUtf8().data(), kCFStringEncodingUTF8);
    QScopeGuard freeFolder([folderCFStr]() { CFRelease(folderCFStr); });

    CFURLRef urlRef = CFURLCreateWithFileSystemPath(nullptr, folderCFStr, kCFURLPOSIXPathStyle, true);
    QScopeGuard freeUrl([urlRef]() { CFRelease(urlRef); });

    LSSharedFileListRef placesItems = LSSharedFileListCreate(nullptr, kLSSharedFileListFavoriteItems, nullptr);
    QScopeGuard freePlaces([placesItems]() { CFRelease(placesItems); });

    if (placesItems) {
        //Insert an item to the list.
        if (LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(placesItems,
                kLSSharedFileListItemLast, nullptr, nullptr, urlRef, nullptr, nullptr)) {
            CFRelease(item);
        }
    }
}

bool Utility::hasSystemLaunchOnStartup(const QString &appName)
{
    Q_UNUSED(appName)
    return false;
}

static Result<void, QString> writePlistToFile(NSString *plistFile, NSDictionary *plist)
{
    NSError *error = nil;

    // Check if the directory exists. On a fresh installation for example, the directory does not
    // exist, so writing the plist file below will fail.
    QDir plistDir = QFileInfo(QString::fromNSString(plistFile)).dir();
    if (!plistDir.exists()) {
        if (!QDir().mkpath(plistDir.path())) {
            return QString(QStringLiteral("Failed to create directory '%1'")).arg(plistDir.path());
        }

        // Permissions always seem to be 0700, so set that.
        // Note: the Permission enum documentation states that on unix the owner permissions are
        // returned, but: "This behavior might change in a future Qt version." So we play it safe,
        // and set both the user and the owner permissions to rwx.
        if (!QFile(plistDir.path()).setPermissions(QFileDevice::ReadOwner | QFileDevice::ReadUser | QFileDevice::WriteOwner | QFileDevice::WriteUser | QFileDevice::ExeOwner | QFileDevice::ExeUser)) {
            qCInfo(lcUtility()) << "Failed to set directory permmissions for" << plistDir.path();
        }
    }

    // Now write the file.
    qCInfo(lcUtility()) << "Writing plist file" << QString::fromNSString(plistFile); // Especially for branded clients: log the file name, so it can be found when debugging.
    if (![plist writeToURL:[NSURL fileURLWithPath:plistFile isDirectory:NO] error:&error]) {
        return QString::fromNSString(error.localizedDescription);
    }

    return {};
}

static Result<NSDictionary *, QString> readPlistFromFile(NSString *plistFile)
{
    NSError *error = nil;
    if (NSDictionary *plist = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:plistFile isDirectory:NO] error:&error]) {
        return plist;
    } else {
        return QString::fromNSString(error.localizedDescription);
    }
}

bool Utility::hasLaunchOnStartup(const QString &appName)
{
    Q_UNUSED(appName)

    @autoreleasepool {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSString *appIdentifier = QCoreApplication::organizationDomain().toNSString();
        NSString *plistFile = [NSHomeDirectory() stringByAppendingFormat:@"/Library/LaunchAgents/%@.plist", appIdentifier];

        if ([fileManager fileExistsAtPath:plistFile]) {
            auto maybePlist = readPlistFromFile(plistFile);
            if (!maybePlist) {
                qCInfo(lcUtility()).nospace() << "Cannot read '" << QString::fromNSString(plistFile)
                                              << "', probably not a valid plist file";
                return false;
            }

            if (NSDictionary *plist = *maybePlist) {
                // yes, there is a valid plist file...
                if (id label = plist[@"Label"]) {
                    // ... with a label...
                    if ([appIdentifier isEqualToString:label]) {
                        // ... and yes, it's the correct app-id...
                        if (id program = plist[@"Program"]) {
                            // .. and there is a program mentioned ...
                            // (Note: case insensitive compare, because most fs setups on mac are case insensitive)
                            if ([QCoreApplication::applicationFilePath().toNSString() compare:program options:NSCaseInsensitiveSearch] == NSOrderedSame) {
                                // ... and it's our executable ..
                                if (NSNumber *value = plist[@"RunAtLoad"]) {
                                    // yes, there is even a RunAtLoad key, so use it!
                                    return [value boolValue];
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return false;
}

static Result<void, QString> writeNewPlistFile(NSString *plistFile, NSString *fullPath, bool enable)
{
    NSDictionary *plistTemplate = @{
        @"Label" : QCoreApplication::organizationDomain().toNSString(),
        @"KeepAlive" : @NO,
        @"Program" : fullPath,
        @"RunAtLoad" : enable ? @YES : @NO
    };

    return writePlistToFile(plistFile, plistTemplate);
}

static Result<void, QString> modifyPlist(NSString *plistFile, NSDictionary *plist, bool enable)
{
    if (NSNumber *value = plist[@"RunAtLoad"]) {
        // ok, there is a key
        if ([value boolValue] == enable) {
            // nothing to do
            return {};
        }
    }

    // now either the key was missing, or it had the wrong value, so set the key and write the plist back
    NSMutableDictionary *newPlist = [plist mutableCopy];
    newPlist[@"RunAtLoad"] = enable ? @YES : @NO;
    return writePlistToFile(plistFile, newPlist);
}

void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
{
    Q_UNUSED(appName)
    Q_UNUSED(guiName)

    @autoreleasepool {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSString *fullPath = QCoreApplication::applicationFilePath().toNSString();
        NSString *appIdentifier = QCoreApplication::organizationDomain().toNSString();
        NSString *plistFile = [NSHomeDirectory() stringByAppendingFormat:@"/Library/LaunchAgents/%@.plist", appIdentifier];

        // An error might occur in the code below, but we cannot report anything, so we just ignore them.

        if ([fileManager fileExistsAtPath:plistFile]) {
            auto maybePlist = readPlistFromFile(plistFile);
            if (!maybePlist) {
                // broken plist, overwrite it
                auto result = writeNewPlistFile(plistFile, fullPath, enable);
                if (!result) {
                    qCWarning(lcUtility) << Q_FUNC_INFO << result.error();
                }
                return;
            }
            NSDictionary *plist = *maybePlist;

            id programValue = plist[@"Program"];
            if (programValue == nil) {
                // broken plist, overwrite it
                auto result = writeNewPlistFile(plistFile, fullPath, enable);
                if (!result) {
                    qCWarning(lcUtility) << result.error();
                }
            } else if (![fileManager fileExistsAtPath:programValue]) {
                // Ok, a plist from some removed program, overwrite it
                auto result = writeNewPlistFile(plistFile, fullPath, enable);
                if (!result) {
                    qCWarning(lcUtility) << result.error();
                }
            } else if ([fullPath compare:programValue options:NSCaseInsensitiveSearch] == NSOrderedSame) { // (Note: case insensitive compare, because most fs setups on mac are case insensitive)
                // Wohoo, it's ours! Now carefully change only the RunAtLoad entry. If any value for
                // e.g. KeepAlive was changed, we leave it as-is.
                auto result = modifyPlist(plistFile, plist, enable);
                if (!result) {
                    qCWarning(lcUtility) << result.error();
                }
            } else if ([fullPath hasPrefix:@"/Applications/"]) {
                // ok, we seem to be an officially installed application, overwrite the file
                auto result = writeNewPlistFile(plistFile, fullPath, enable);
                if (!result) {
                    qCWarning(lcUtility) << result.error();
                }
            } else {
                qCInfo(lcUtility) << "We're not an installed application, there is anoter executable "
                                     "mentioned in the plist file, and that executable seems to exist, "
                                     "so let's not touch the file.";
            }
        } else {
            if (enable) {
                // plist doens't exist, write a new one.
                auto result = writeNewPlistFile(plistFile, fullPath, enable);
                if (!result) {
                    qCWarning(lcUtility) << result.error();
                }
            } else {
                // Do nothing: if the file doesn't exist, the client won't be auto-started.
            }
        }
    }
}

#ifndef TOKEN_AUTH_ONLY
bool Utility::hasDarkSystray()
{
    @autoreleasepool {
        if (auto style = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]) {
            return [style isEqualToString:@"Dark"];
        }
    }

    return false;
}
#endif

QString Utility::currentCpuArch()
{
    static const QString rv = []() {
        int procTranslated = 0;
        size_t size = sizeof(procTranslated);

        const auto rv = sysctlbyname("sysctl.proc_translated", &procTranslated, &size, nullptr, 0);

        // should be backed up if not evaluated immediately
        const auto error = errno;

        qCDebug(lcUtility) << "proc_translated:" << procTranslated;
        qCDebug(lcUtility) << "error:" << strerror(error);

        if (rv != -1) {
            if (procTranslated == 1) {
                qCInfo(lcUtility) << "process translated with rosetta2";
                return QStringLiteral("arm64");
            } else {
                qCInfo(lcUtility) << "process not translated";
            }
        } else {
            if (error == ENOENT) {
                qCDebug(lcUtility) << "not running on Apple Silicon";
            } else {
                qCDebug(lcUtility) << "unknown error trying to detect whether proc is translated";
            }
        }

        const auto qtArch = QSysInfo::currentCpuArchitecture();
        qCDebug(lcUtility) << "CPU arch reported by Qt:" << qtArch;
        return qtArch;
    }();

    return rv;
}

} // namespace OCC
