//
//  FileData.cpp
//
//  Provides game file data structures and functions to access and sort this information.
//  Also provides functions to look up paths to media files and for launching games
//  (launching initiated by the ViewController).
//

#include "FileData.h"

#include "guis/GuiInfoPopup.h"
#include "utils/FileSystemUtil.h"
#include "utils/StringUtil.h"
#include "utils/TimeUtil.h"
#include "AudioManager.h"
#include "CollectionSystemManager.h"
#include "FileFilterIndex.h"
#include "FileSorts.h"
#include "Log.h"
#include "MameNames.h"
#include "Platform.h"
#include "Scripting.h"
#include "SystemData.h"
#include "VolumeControl.h"
#include "Window.h"

#include <assert.h>

FileData::FileData(
        FileType type,
        const std::string& path,
        SystemEnvironmentData* envData,
        SystemData* system)
        : mType(type),
        mPath(path),
        mSystem(system),
        mEnvData(envData),
        mSourceFileData(nullptr),
        mParent(nullptr),
        mOnlyFolders(false),
        mDeletionFlag(false),
        // Metadata is REALLY set in the constructor!
        metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA)
{
    // Metadata needs at least a name field (since that's what getName() will return).
    if (metadata.get("name").empty()) {
        if ((system->hasPlatformId(PlatformIds::ARCADE) ||
                system->hasPlatformId(PlatformIds::NEOGEO)) &&
                metadata.getType() != FOLDER_METADATA) {
            // If it's a MAME or Neo Geo game, expand the game name accordingly.
            metadata.set("name",
                    MameNames::getInstance()->getCleanName(getCleanName()));
        }
        else {
            if (metadata.getType() == FOLDER_METADATA && Utils::FileSystem::isHidden(mPath)) {
                metadata.set("name", Utils::FileSystem::getFileName(mPath));
            }
            else {
                metadata.set("name", getDisplayName());
            }
        }
    }
    mSystemName = system->getName();
    metadata.resetChangedFlag();
}

FileData::~FileData()
{
    if (mParent)
        mParent->removeChild(this);

    if (mType == GAME)
        mSystem->getIndex()->removeFromIndex(this);

    mChildren.clear();
}

std::string FileData::getDisplayName() const
{
    std::string stem = Utils::FileSystem::getStem(mPath);
    return stem;
}

std::string FileData::getCleanName() const
{
    return Utils::String::removeParenthesis(this->getDisplayName());
}

const std::string& FileData::getName()
{
    return metadata.get("name");
}

const std::string& FileData::getSortName()
{
    if (metadata.get("sortname").empty())
        return metadata.get("name");
    else
        return metadata.get("sortname");
}

const bool FileData::getFavorite()
{
    if (metadata.get("favorite") == "true")
        return true;
    else
        return false;
}

const bool FileData::getHidden()
{
    if (metadata.get("hidden") == "true")
        return true;
    else
        return false;
}

const bool FileData::getCountAsGame()
{
    if (metadata.get("nogamecount") == "true")
        return false;
    else
        return true;
}

const bool FileData::getExcludeFromScraper()
{
    if (metadata.get("nomultiscrape") == "true")
        return true;
    else
        return false;
}

const std::vector<FileData*> FileData::getChildrenRecursive() const
{
    std::vector<FileData*> childrenRecursive;

    for (auto it = mChildrenByFilename.cbegin();
            it != mChildrenByFilename.cend(); it++) {
        childrenRecursive.push_back((*it).second);
        // Recurse through any subdirectories.
        if ((*it).second->getType() == FOLDER) {
            std::vector<FileData*> childrenSubdirectory = (*it).second->getChildrenRecursive();
            childrenRecursive.insert(childrenRecursive.end(),
                    childrenSubdirectory.begin(), childrenSubdirectory.end());
        }
    }

    return childrenRecursive;
}

const std::string FileData::getROMDirectory()
{
    std::string romDirSetting = Settings::getInstance()->getString("ROMDirectory");
    std::string romDirPath = "";

    if (romDirSetting == "") {
        romDirPath = Utils::FileSystem::getHomePath() + "/ROMs/";
    }
    else {
        romDirPath = romDirSetting;
        // Expand home path if ~ is used.
        romDirPath = Utils::FileSystem::expandHomePath(romDirPath);

        if (romDirPath.back() !=  '/')
            romDirPath = romDirPath + "/";
    }

    // If %ESPATH% is used for the ROM path configuration, then expand it to the executable
    // directory of ES. This is useful for a portable emulator installation, for instance on
    // a USB memory stick.
    romDirPath = Utils::String::replace(romDirPath, "%ESPATH%", Utils::FileSystem::getExePath());

    return romDirPath;
}

const std::string FileData::getMediaDirectory()
{
    std::string mediaDirSetting = Settings::getInstance()->getString("MediaDirectory");
    std::string mediaDirPath = "";

    if (mediaDirSetting == "") {
        mediaDirPath = Utils::FileSystem::getHomePath() + "/.emulationstation/downloaded_media/";
    }
    else {
        mediaDirPath = mediaDirSetting;
        // Expand home path if ~ is used.
        mediaDirPath = Utils::FileSystem::expandHomePath(mediaDirPath);
        // Expand home symbol if the path starts with ~

        if (mediaDirPath.back() !=  '/')
            mediaDirPath = mediaDirPath + "/";
    }

    return mediaDirPath;
}

const std::string FileData::getMediafilePath(std::string subdirectory, std::string mediatype) const
{
    const char* extList[2] = { ".png", ".jpg" };

    // Look for an image file in the media directory.
    std::string tempPath = getMediaDirectory() + mSystemName + "/" +
            subdirectory + "/" + getDisplayName();
    for (int i = 0; i < 2; i++) {
        std::string mediaPath = tempPath + extList[i];
        if (Utils::FileSystem::exists(mediaPath))
            return mediaPath;
    }

    // No media found in the media directory, so look
    // for local art as well (if configured to do so).
    if (Settings::getInstance()->getBool("LocalArt")) {
        for (int i = 0; i < 2; i++) {
            std::string localMediaPath = mEnvData->mStartPath + "/images/" +
                    getDisplayName() + "-" + mediatype + extList[i];
            if (Utils::FileSystem::exists(localMediaPath))
                return localMediaPath;
        }
    }

    return "";
}

const std::string FileData::getImagePath() const
{
    // Look for a mix image (a combination of screenshot, 2D/3D box and marquee).
    std::string image = getMediafilePath("miximages", "miximage");
    if (image != "")
        return image;

    // If no mix image was found, try screenshot instead.
    image = getMediafilePath("screenshots", "screenshot");
    if (image != "")
        return image;

    // If no screenshot was found either, try cover.
    return getMediafilePath("covers", "cover");
}

const std::string FileData::get3DBoxPath() const
{
    return getMediafilePath("3dboxes", "3dbox");
}

const std::string FileData::getCoverPath() const
{
    return getMediafilePath("covers", "cover");
}

const std::string FileData::getMarqueePath() const
{
    return getMediafilePath("marquees", "marquee");
}

const std::string FileData::getMiximagePath() const
{
    return getMediafilePath("miximages", "miximage");
}

const std::string FileData::getScreenshotPath() const
{
    return getMediafilePath("screenshots", "screenshot");
}

const std::string FileData::getThumbnailPath() const
{
    return getMediafilePath("thumbnails", "thumbnail");
}

const std::string FileData::getVideoPath() const
{
    const char* extList[5] = { ".avi", ".mkv", ".mov", ".mp4", ".wmv" };
    std::string tempPath = getMediaDirectory() + mSystemName + "/videos/" + getDisplayName();

    // Look for media in the media directory.
    for (int i = 0; i < 5; i++) {
        std::string mediaPath = tempPath + extList[i];
        if (Utils::FileSystem::exists(mediaPath))
            return mediaPath;
    }

    // No media found in the media directory, so look
    // for local art as well (if configured to do so).
    if (Settings::getInstance()->getBool("LocalArt"))
    {
        for (int i = 0; i < 5; i++) {
            std::string localMediaPath = mEnvData->mStartPath + "/videos/" + getDisplayName() +
                    "-video" + extList[i];
            if (Utils::FileSystem::exists(localMediaPath))
                return localMediaPath;
        }
    }

    return "";
}

const std::vector<FileData*>& FileData::getChildrenListToDisplay()
{

    FileFilterIndex* idx = CollectionSystemManager::get()->getSystemToView(mSystem)->getIndex();
    if (idx->isFiltered()) {
        mFilteredChildren.clear();
        for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) {
            if (idx->showFile((*it))) {
                mFilteredChildren.push_back(*it);
            }
        }
        return mFilteredChildren;
    }
    else {
        return mChildren;
    }
}

std::vector<FileData*> FileData::getFilesRecursive(unsigned int typeMask,
        bool displayedOnly, bool countAllGames) const
{
    std::vector<FileData*> out;
    FileFilterIndex* idx = mSystem->getIndex();

    for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) {
        if ((*it)->getType() & typeMask) {
            if (!displayedOnly || !idx->isFiltered() || idx->showFile(*it)) {
                if (countAllGames)
                    out.push_back(*it);
                else if ((*it)->getCountAsGame())
                    out.push_back(*it);
            }
        }
        if ((*it)->getChildren().size() > 0) {
            std::vector<FileData*> subChildren = (*it)->getFilesRecursive(typeMask, displayedOnly);
            if (countAllGames) {
                out.insert(out.cend(), subChildren.cbegin(), subChildren.cend());
            }
            else {
                for (auto it = subChildren.cbegin(); it != subChildren.cend(); it++) {
                    if ((*it)->getCountAsGame())
                        out.push_back(*it);
                }
            }
        }
    }

    return out;
}

std::vector<FileData*> FileData::getScrapeFilesRecursive(bool includeFolders,
        bool excludeRecursively, bool respectExclusions) const
{
    std::vector<FileData*> out;

    for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) {
        if (includeFolders && (*it)->getType() == FOLDER) {
            if (!(respectExclusions && (*it)->getExcludeFromScraper()))
                out.push_back(*it);
        }
        else if ((*it)->getType() == GAME) {
            if (!(respectExclusions && (*it)->getExcludeFromScraper()))
                out.push_back(*it);
        }

        // If the flag has been passed to exclude directories recursively, then skip the entire
        // folder at this point if the folder is marked for scrape exclusion.
        if (excludeRecursively && (*it)->getType() == FOLDER && (*it)->getExcludeFromScraper())
            continue;

        if ((*it)->getChildren().size() > 0) {
            std::vector<FileData*> subChildren = (*it)->getScrapeFilesRecursive(
                    includeFolders, excludeRecursively, respectExclusions);
            out.insert(out.cend(), subChildren.cbegin(), subChildren.cend());
        }
    }

    return out;
}

std::string FileData::getKey() {
    return getFileName();
}

const bool FileData::isArcadeAsset()
{
    const std::string stem = Utils::FileSystem::getStem(mPath);
    return (
        (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) ||
                mSystem->hasPlatformId(PlatformIds::NEOGEO))) &&
                (MameNames::getInstance()->isBios(stem) ||
                MameNames::getInstance()->isDevice(stem))
    );
}

FileData* FileData::getSourceFileData()
{
    return this;
}

void FileData::addChild(FileData* file)
{
    assert(mType == FOLDER);
    assert(file->getParent() == nullptr);

    const std::string key = file->getKey();
    if (mChildrenByFilename.find(key) == mChildrenByFilename.cend()) {
        mChildrenByFilename[key] = file;
        mChildren.push_back(file);
        file->mParent = this;
    }
}

void FileData::removeChild(FileData* file)
{
    assert(mType == FOLDER);
    assert(file->getParent() == this);
    mChildrenByFilename.erase(file->getKey());
    for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) {
        if (*it == file) {
            file->mParent = nullptr;
            mChildren.erase(it);
            return;
        }
    }

    // File somehow wasn't in our children.
    assert(false);
}

void FileData::sort(ComparisonFunction& comparator, bool ascending)
{
    mFirstLetterIndex.clear();
    mOnlyFolders = true;
    bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop");
    std::vector<FileData*> mChildrenFolders;
    std::vector<FileData*> mChildrenOthers;

    // Only run this section of code if the setting to show hidden games has been disabled,
    // in order to avoid unnecessary processing.
    if (!Settings::getInstance()->getBool("ShowHiddenGames")) {
        std::vector<FileData*> mChildrenShown;
        for (unsigned int i = 0; i < mChildren.size(); i++) {
            if (mChildren[i]->getHidden()) {
                LOG(LogDebug) << "FileData::sort(): Skipping hidden game '" <<
                        mChildren[i]->getName() << "'" << " (" << mChildren[i]->getPath() << ").";
                continue;
            }
            mChildrenShown.push_back(mChildren[i]);
        }
        mChildren.erase(mChildren.begin(), mChildren.end());
        mChildren.reserve(mChildrenShown.size());
        mChildren.insert(mChildren.end(), mChildrenShown.begin(), mChildrenShown.end());
    }

    if (foldersOnTop) {
        for (unsigned int i = 0; i < mChildren.size(); i++) {
            if (mChildren[i]->getType() == FOLDER)
                mChildrenFolders.push_back(mChildren[i]);
            else
                mChildrenOthers.push_back(mChildren[i]);
        }

        std::stable_sort(mChildrenFolders.begin(), mChildrenFolders.end(), comparator);
        std::stable_sort(mChildrenOthers.begin(), mChildrenOthers.end(), comparator);

        if (!ascending) {
            std::reverse(mChildrenFolders.begin(), mChildrenFolders.end());
            std::reverse(mChildrenOthers.begin(), mChildrenOthers.end());
        }

        mChildren.erase(mChildren.begin(), mChildren.end());
        mChildren.reserve(mChildrenFolders.size() + mChildrenOthers.size());
        mChildren.insert(mChildren.end(), mChildrenFolders.begin(), mChildrenFolders.end());
        mChildren.insert(mChildren.end(), mChildrenOthers.begin(), mChildrenOthers.end());
    }
    else {
        std::stable_sort(mChildren.begin(), mChildren.end(), comparator);
        if (!ascending)
            std::reverse(mChildren.begin(), mChildren.end());
    }

    for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) {
        if ((*it)->getType() != FOLDER)
            mOnlyFolders = false;

        if (!(foldersOnTop && (*it)->getType() == FOLDER)) {
            // Build mFirstLetterIndex.
            const char firstChar = toupper((*it)->getSortName().front());
            mFirstLetterIndex.push_back(std::string(1, firstChar));
        }
        // Iterate through any child folders.
        if ((*it)->getChildren().size() > 0)
            (*it)->sort(comparator, ascending);
    }

    // If there are only folders in the gamelist, then it makes sense to still
    // generate a letter index.
    if (mOnlyFolders) {
        for (unsigned int i = 0; i < mChildrenFolders.size(); i++) {
            const char firstChar = toupper(mChildrenFolders[i]->getSortName().front());
            mFirstLetterIndex.push_back(std::string(1, firstChar));
        }
        mOnlyFolders = true;
    }

    // Sort and make each entry unique in mFirstLetterIndex.
    std::sort(mFirstLetterIndex.begin(), mFirstLetterIndex.end());
    auto last = std::unique(mFirstLetterIndex.begin(), mFirstLetterIndex.end());
    mFirstLetterIndex.erase(last, mFirstLetterIndex.end());
}

void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending)
{
    mFirstLetterIndex.clear();
    mOnlyFolders = true;
    std::vector<FileData*> mChildrenFolders;
    std::vector<FileData*> mChildrenFavorites;
    std::vector<FileData*> mChildrenOthers;
    bool showHiddenGames = Settings::getInstance()->getBool("ShowHiddenGames");
    bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop");

    for (unsigned int i = 0; i < mChildren.size(); i++) {
        // Exclude game if it's marked as hidden and the hide setting has been set.
        if (!showHiddenGames && mChildren[i]->getHidden()) {
            LOG(LogDebug) << "FileData::sortFavoritesOnTop(): Skipping hidden game '" <<
                        mChildren[i]->getName() << "'" << " (" << mChildren[i]->getPath() << ").";
            continue;
        }

        if (foldersOnTop && mChildren[i]->getType() == FOLDER) {
            mChildrenFolders.push_back(mChildren[i]);
        }
        else if (mChildren[i]->getFavorite()) {
            mChildrenFavorites.push_back(mChildren[i]);
        }
        else {
            mChildrenOthers.push_back(mChildren[i]);
            // Build mFirstLetterIndex.
            const char firstChar = toupper(mChildren[i]->getSortName().front());
            mFirstLetterIndex.push_back(std::string(1, firstChar));
        }

        if (mChildren[i]->getType() != FOLDER)
            mOnlyFolders = false;
    }

    // If there are only favorites in the gamelist, it makes sense to still generate
    // a letter index. For instance to be able to quick jump in the 'favorites'
    // collection. Doing this additional work here only for the applicable gamelists is
    // probably faster than building a redundant index for all gamelists during sorting.
    if (mChildrenOthers.size() == 0 && mChildrenFavorites.size() > 0) {
        for (unsigned int i = 0; i < mChildren.size(); i++) {
            if (foldersOnTop && mChildren[i]->getType() == FOLDER) {
                continue;
            }
            else {
                const char firstChar = toupper(mChildren[i]->getSortName().front());
                mFirstLetterIndex.push_back(std::string(1, firstChar));
            }
        }
    }
    // If there are only folders in the gamelist, then it also makes sense to generate
    // a letter index.
    else if (mOnlyFolders) {
        for (unsigned int i = 0; i < mChildrenFolders.size(); i++) {
            const char firstChar = toupper(mChildrenFolders[i]->getSortName().front());
            mFirstLetterIndex.push_back(std::string(1, firstChar));
        }
    }

    // Sort and make each entry unique in mFirstLetterIndex.
    std::sort(mFirstLetterIndex.begin(), mFirstLetterIndex.end());
    auto last = std::unique(mFirstLetterIndex.begin(), mFirstLetterIndex.end());
    mFirstLetterIndex.erase(last, mFirstLetterIndex.end());

    // If there were at least one favorite in the gamelist, insert the favorite
    // unicode character in the first position.
    if (mChildrenOthers.size() > 0 && mChildrenFavorites.size() > 0)
        mFirstLetterIndex.insert(mFirstLetterIndex.begin(), FAVORITE_CHAR);

    // Sort favorite games and the other games separately.
    std::stable_sort(mChildrenFolders.begin(), mChildrenFolders.end(), comparator);
    std::stable_sort(mChildrenFavorites.begin(), mChildrenFavorites.end(), comparator);
    std::stable_sort(mChildrenOthers.begin(), mChildrenOthers.end(), comparator);

    // Iterate through any child folders.
    for (auto it = mChildrenFolders.cbegin(); it != mChildrenFolders.cend(); it++) {
        if ((*it)->getChildren().size() > 0)
            (*it)->sortFavoritesOnTop(comparator, ascending);
    }

    // Iterate through any child folders.
    for (auto it = mChildrenFavorites.cbegin(); it != mChildrenFavorites.cend(); it++) {
        if ((*it)->getChildren().size() > 0)
            (*it)->sortFavoritesOnTop(comparator, ascending);
    }

    // Iterate through any child folders.
    for (auto it = mChildrenOthers.cbegin(); it != mChildrenOthers.cend(); it++) {
        if ((*it)->getChildren().size() > 0)
            (*it)->sortFavoritesOnTop(comparator, ascending);
    }

    if (!ascending) {
        std::reverse(mChildrenFolders.begin(), mChildrenFolders.end());
        std::reverse(mChildrenFavorites.begin(), mChildrenFavorites.end());
        std::reverse(mChildrenOthers.begin(), mChildrenOthers.end());
    }

    // Combine the individually sorted favorite games and other games vectors.
    mChildren.erase(mChildren.begin(), mChildren.end());
    mChildren.reserve(mChildrenFolders.size() + mChildrenFavorites.size() + mChildrenOthers.size());
    mChildren.insert(mChildren.end(), mChildrenFolders.begin(), mChildrenFolders.end());
    mChildren.insert(mChildren.end(), mChildrenFavorites.begin(), mChildrenFavorites.end());
    mChildren.insert(mChildren.end(), mChildrenOthers.begin(), mChildrenOthers.end());
}

void FileData::sort(const SortType& type, bool mFavoritesOnTop)
{
    if (mFavoritesOnTop)
        sortFavoritesOnTop(*type.comparisonFunction, type.ascending);
    else
        sort(*type.comparisonFunction, type.ascending);
}

void FileData::launchGame(Window* window)
{
    LOG(LogInfo) << "Attempting to launch game...";

    std::string command = "";

    // Check if there is a launch command override for the game
    // and the corresponding option to use it has been set.
    if (Settings::getInstance()->getBool("LaunchCommandOverride") &&
            !metadata.get("launchcommand").empty())
        command = metadata.get("launchcommand");
    else
        command = mEnvData->mLaunchCommand;

    std::string commandRaw = command;

    const std::string rom = Utils::FileSystem::getEscapedPath(getPath());
    const std::string basename = Utils::FileSystem::getStem(getPath());
    const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath());
    const std::string emupath = Utils::FileSystem::getExePath();

    command = Utils::String::replace(command, "%ROM%", rom);
    command = Utils::String::replace(command, "%BASENAME%", basename);
    command = Utils::String::replace(command, "%ROM_RAW%", rom_raw);
    command = Utils::String::replace(command, "%ESPATH%", emupath);

    // Expand home path if ~ is used.
    command = Utils::FileSystem::expandHomePath(command);

    #ifdef _WIN64
    std::wstring commandWide = Utils::String::stringToWideString(command);
    #endif

    Scripting::fireEvent("game-start", rom, getSourceFileData()->metadata.get("name"));
    int returnValue = 0;

    if (command.find("%EMUPATH%") != std::string::npos) {
        // Extract the emulator executable from the launch command string. This could either be
        // just the program name, assuming the binary is in the PATH variable of the operating
        // system, or it could be an absolute path to the emulator. (In the latter case, if
        // there is a space in the the path, it needs to be enclosed by quotation marks in
        // es_systems.cfg.)
        std::string emuExecutable;

        // If the first character is a quotation mark, then we need to extract up to the
        // next quotation mark, otherwise we'll extract up to the first space character.
        if (command.front() == '\"') {
            std::string emuTemp = command.substr(1, std::string::npos);
            emuExecutable = emuTemp.substr(0, emuTemp.find('"'));
        }
        else {
            emuExecutable = command.substr(0, command.find(' '));
        }

        // For Windows, we need to handle UTF-16 encoding.
        #ifdef _WIN64
        std::wstring emuExecutableWide;
        std::wstring emuPathWide;

        emuExecutableWide = Utils::String::stringToWideString(emuExecutable);

        // Search for the emulator using the PATH environmental variable.
        DWORD size = SearchPathW(nullptr, emuExecutableWide.c_str(), L".exe", 0, nullptr, nullptr);

        if (size) {
            std::vector<wchar_t> pathBuffer(static_cast<size_t>(size) + 1 );
            wchar_t* fileName = nullptr;

            SearchPathW(nullptr, emuExecutableWide.c_str(), L".exe", size + 1 ,
                    pathBuffer.data(), &fileName);
            std::wstring pathString = pathBuffer.data();

            if (pathString.length()) {
                emuPathWide = pathString.substr(0, pathString.size() -
                        std::wstring(fileName).size());
                emuPathWide.pop_back();
                auto stringPos = commandWide.find(L"%EMUPATH%");
                commandWide = commandWide.replace(stringPos, 9, emuPathWide);
            }
        }
        #else
        std::string exePath;
        if (Utils::FileSystem::isRegularFile(emuExecutable) ||
                Utils::FileSystem::isSymlink(emuExecutable))
            exePath = Utils::FileSystem::getParent(emuExecutable);
        else
            exePath = Utils::FileSystem::getPathToBinary(emuExecutable);

        command = Utils::String::replace(command, "%EMUPATH%", exePath);
        #endif
    }

    LOG(LogDebug) << "Raw emulator launch command:";
    LOG(LogDebug) << commandRaw;
    LOG(LogInfo) << "Expanded emulator launch command:";

    #ifdef _WIN64
    LOG(LogInfo) << Utils::String::wideStringToString(commandWide);
    returnValue = launchEmulatorWindows(commandWide);
    #else
    LOG(LogInfo) << command;
    returnValue = launchEmulatorUnix(command);
    #endif

    // Notify the user in case of a failed game launch using a popup window.
    if (returnValue != 0) {
        LOG(LogWarning) << "...launch terminated with nonzero return value " << returnValue << "!";

        GuiInfoPopup* s = new GuiInfoPopup(window, "ERROR LAUNCHING GAME '" +
                Utils::String::toUpper(metadata.get("name")) + "' (ERROR CODE " +
                Utils::String::toUpper(std::to_string(returnValue) + ")"), 6000);
        window->setInfoPopup(s);
    }
    else {
        #ifdef _WIN64
        // This code is only needed for Windows, where we may need to keep ES running while
        // the game/emulator is in use. It's basically used to pause any playing game video
        // and to keep the screensaver from activating.
        if (Settings::getInstance()->getBool("RunInBackground"))
            window->setLaunchedGame();
        else
            // Normalize deltaTime so that the screensaver does not start immediately
            // when returning from the game.
            window->normalizeNextUpdate();
        #else
        // Normalize deltaTime so that the screensaver does not start immediately
        // when returning from the game.
        window->normalizeNextUpdate();
        #endif
    }

    Scripting::fireEvent("game-end", rom, getSourceFileData()->metadata.get("name"));

    // Update number of times the game has been launched.
    FileData* gameToUpdate = getSourceFileData();

    int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1;
    gameToUpdate->metadata.set("playcount", std::to_string(static_cast<long long>(timesPlayed)));

    // Update last played time.
    gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now()));

    // If the parent is a folder and it's not the root of the system, then update its lastplayed
    // timestamp to the same time as the game that was just launched.
    if (gameToUpdate->getParent()->getType() == FOLDER && gameToUpdate->getParent()->getName() !=
            gameToUpdate->getSystem()->getFullName()) {
        gameToUpdate->getParent()->metadata.set("lastplayed",
                gameToUpdate->metadata.get("lastplayed"));
    }

    CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate);

    gameToUpdate->mSystem->onMetaDataSavePoint();
}

CollectionFileData::CollectionFileData(FileData* file, SystemData* system)
    : FileData(file->getSourceFileData()->getType(), file->getSourceFileData()->getPath(),
            file->getSourceFileData()->getSystemEnvData(), system)
{
    // We use this constructor to create a clone of the filedata, and change its system.
    mSourceFileData = file->getSourceFileData();
    refreshMetadata();
    mParent = nullptr;
    metadata = mSourceFileData->metadata;
    mSystemName = mSourceFileData->getSystem()->getName();
}

CollectionFileData::~CollectionFileData()
{
    // Need to remove collection file data at the collection object destructor.
    if (mParent)
        mParent->removeChild(this);
    mParent = nullptr;
}

std::string CollectionFileData::getKey() {
    return getFullPath();
}

FileData* CollectionFileData::getSourceFileData()
{
    return mSourceFileData;
}

void CollectionFileData::refreshMetadata()
{
    metadata = mSourceFileData->metadata;
    mDirty = true;
}

const std::string& CollectionFileData::getName()
{
    if (mDirty) {
        mCollectionFileName =
                Utils::String::removeParenthesis(mSourceFileData->metadata.get("name"));
        mCollectionFileName +=
                " [" + Utils::String::toUpper(mSourceFileData->getSystem()->getName()) + "]";
        mDirty = false;
    }

    if (Settings::getInstance()->getBool("CollectionShowSystemInfo"))
        return mCollectionFileName;

    return mSourceFileData->metadata.get("name");
}

// Return sort type based on a string description.
FileData::SortType getSortTypeFromString(std::string desc) {
    std::vector<FileData::SortType> SortTypes = FileSorts::SortTypes;
    // Find it
    for (unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) {
        const FileData::SortType& sort = FileSorts::SortTypes.at(i);
        if (sort.description == desc)
            return sort;
    }
    // If no type found then default to "filename, ascending".
    return FileSorts::SortTypes.at(0);
}