/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 *   Copyright (C) 2020 by Dominik Riebeling
 *
 * All files in this archive are subject to the GNU General Public License.
 * See the file COPYING in the source tree root for full license agreement.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#include "playerbuildinfo.h"
#include "rbsettings.h"
#include "Logger.h"

PlayerBuildInfo* PlayerBuildInfo::infoInstance = nullptr;

PlayerBuildInfo* PlayerBuildInfo::instance()
{
    if (infoInstance == nullptr) {
        infoInstance = new PlayerBuildInfo();
    }
    return infoInstance;
}

// server infos
const static struct {
    PlayerBuildInfo::BuildInfo item;
    const char* name;
} ServerInfoList[] = {
    { PlayerBuildInfo::BuildVoiceLangs,   "voices/:version:"    },
    { PlayerBuildInfo::BuildVersion,      ":build:/:target:"     },
    { PlayerBuildInfo::BuildUrl,          ":build:/build_url"   },
    { PlayerBuildInfo::BuildVoiceUrl,     ":build:/voice_url"   },
    { PlayerBuildInfo::BuildManualUrl,    ":build:/manual_url"  },
    { PlayerBuildInfo::BuildSourceUrl,    ":build:/source_url"  },
    { PlayerBuildInfo::BuildFontUrl,      ":build:/font_url"    },

    // other URLs -- those are not directly related to the build, but handled here.
    { PlayerBuildInfo::DoomUrl,           "other/doom_url"      },
    { PlayerBuildInfo::Duke3DUrl,         "other/duke3d_url"    },
    { PlayerBuildInfo::PuzzFontsUrl,      "other/puzzfonts_url" },
    { PlayerBuildInfo::QuakeUrl,          "other/quake_url"     },
    { PlayerBuildInfo::Wolf3DUrl,         "other/wolf3d_url"    },
    { PlayerBuildInfo::XWorldUrl,         "other/xworld_url"    },
    { PlayerBuildInfo::MidiPatchsetUrl,   "other/patcheset_url" },
};

const static struct {
    PlayerBuildInfo::DeviceInfo item;
    const char* name;
} PlayerInfoList[] = {
    { PlayerBuildInfo::BuildStatus,        "status/:target:"           },
    { PlayerBuildInfo::DisplayName,        ":target:/name"             },
    { PlayerBuildInfo::BootloaderMethod,   ":target:/bootloadermethod" },
    { PlayerBuildInfo::BootloaderName,     ":target:/bootloadername"   },
    { PlayerBuildInfo::BootloaderFile,     ":target:/bootloaderfile"   },
    { PlayerBuildInfo::BootloaderFilter,   ":target:/bootloaderfilter" },
    { PlayerBuildInfo::Encoder,            ":target:/encoder"          },
    { PlayerBuildInfo::Brand,              ":target:/brand"            },
    { PlayerBuildInfo::PlayerPicture,      ":target:/playerpic"        },
    { PlayerBuildInfo::TargetNamesAll,     "_targets/all"              },
    { PlayerBuildInfo::TargetNamesEnabled, "_targets/enabled"          },
    { PlayerBuildInfo::LanguageInfo,       "languages/:target:"        },
    { PlayerBuildInfo::LanguageList,       "_languages/list"           },
    { PlayerBuildInfo::UsbIdErrorList,     "_usb/error"                },
    { PlayerBuildInfo::UsbIdTargetList,    "_usb/target"               },
};

const static struct {
    PlayerBuildInfo::SystemUrl item;
    const char* name;
} PlayerSystemUrls[] = {
    { PlayerBuildInfo::BootloaderUrl,     "bootloader/download_url" },
    { PlayerBuildInfo::BuildInfoUrl,      "build_info_url"          },
    { PlayerBuildInfo::GenlangUrl,        "genlang_url"             },
    { PlayerBuildInfo::ThemesUrl,         "themes_url"              },
    { PlayerBuildInfo::ThemesInfoUrl,     "themes_info_url"         },
    { PlayerBuildInfo::RbutilUrl,         "rbutil_url"              },
};

PlayerBuildInfo::PlayerBuildInfo() :
     serverInfo(nullptr),
     playerInfo(":/ini/rbutil.ini", QSettings::IniFormat)
{

}

void PlayerBuildInfo::setBuildInfo(QString file)
{
    if (serverInfo)
        delete serverInfo;
    LOG_INFO() << "updated:" << file;
    serverInfo = new QSettings(file, QSettings::IniFormat);
}

QVariant PlayerBuildInfo::value(BuildInfo item, BuildType type)
{
    // locate setting item in server info file
    int i = 0;
    while(ServerInfoList[i].item != item)
        i++;

    // split of variant for target.
    // we can have an optional variant part in the target string.
    // For build info we don't use that.
    QString target = RbSettings::value(RbSettings::CurrentPlatform).toString().split('.').at(0);

    QString serverinfo = ServerInfoList[i].name;
    serverinfo.replace(":target:", target);
    QString buildtypename;
    switch(type) {
    case TypeRelease:
        buildtypename = "release";
        break;
    case TypeCandidate:
        buildtypename = "release-candidate";
        break;
    case TypeDaily:
        buildtypename = "daily";
        break;
    case TypeDevel:
        buildtypename = "development";
        // manual and fonts don't exist for development builds. We do have an
        // URL configured, but need to get the daily version instead.
        if(item == BuildManualUrl || item == BuildFontUrl) {
            LOG_INFO() << "falling back to daily build for this info value";
            buildtypename = "daily";
        }
        break;
    }

    QVariant result;
    if (!serverInfo)
        return QString();
    QStringList version = serverInfo->value(buildtypename + "/" + target, "").toStringList();
    serverinfo.replace(":build:", buildtypename);
    serverinfo.replace(":version:", version.at(0));

    // get value from server build-info
    // we need to get a version string, otherwise the data is invalid.
    // For invalid data return an empty string.
    if(version.at(0).isEmpty()) {
        LOG_INFO() << serverinfo << "(version invalid)";
        return QString();
    }
    if(!serverinfo.isEmpty())
        result = serverInfo->value(serverinfo);

    // depending on the actual value we need more replacements.
    switch(item) {
    case BuildVersion:
        result = result.toStringList().at(0);
        break;

    case BuildUrl:
        if(version.size() > 1) {
            // version info has an URL appended. Takes precendence.
            result = version.at(1);
        }
        break;

    case BuildVoiceLangs:
        if (type == TypeDaily)
            serverinfo = "voices/daily";
        result = serverInfo->value(serverinfo);
        break;

    case BuildManualUrl:
    {
        // special case: if playerInfo has a non-empty manualname entry for the
        // target, use that as target for the manual name.
        QString manualtarget = playerInfo.value(target + "/manualname", "").toString();
        if(!manualtarget.isEmpty())
            target = manualtarget;
        break;
    }

    default:
        break;
    }
    // if the value is a string we can replace some patterns.
    // if we cannot convert it (f.e. for a QStringList) we leave as-is, since
    // the conversion would return an empty type.
#if QT_VERSION < 0x060000
    if (result.type() == QVariant::String)
#else
    if (result.metaType().id() == QMetaType::QString)
#endif
        result = result.toString()
                    .replace("%TARGET%", target)
                    .replace("%VERSION%", version.at(0));

    LOG_INFO() << "B:" << serverinfo << result;
    return result;
}

QVariant PlayerBuildInfo::value(DeviceInfo item, QString target)
{
    // locate setting item in server info file
    int i = 0;
    while(PlayerInfoList[i].item != item)
        i++;

    // split of variant for target.
    // we can have an optional variant part in the target string.
    // For device info we use this.
    if (target.isEmpty())
        target = RbSettings::value(RbSettings::CurrentPlatform).toString();

    QVariant result = QString();

    QString s = PlayerInfoList[i].name;
    s.replace(":target:", target);

    switch(item) {
    case BuildStatus:
    {
        // build status is the only value that doesn't depend on the version
        // but the selected target instead.
        bool ok = false;
        if (serverInfo)
            result = serverInfo->value(s).toInt(&ok);
        if (!ok)
            result = -1;
        break;
    }
    case TargetNamesAll:
        // list of all internal target names. Doesn't depend on the passed target.
        result = targetNames(true);
        break;
    case TargetNamesEnabled:
        // list of all non-disabled target names. Doesn't depend on the passed target.
        result = targetNames(false);
        break;

    case LanguageList:
        // Return a map (language, display string).
        {
            // need to use (QString, QVariant) here, so we can put the map into
            // a QVariant by itself.
            QMap<QString, QVariant> m;

            playerInfo.beginGroup("languages");
            QStringList a = playerInfo.childKeys();

            for(int i = 0; i < a.size(); i++) {
                QStringList v = playerInfo.value(a.at(i)).toStringList();
                m[v.at(0)] = v.at(1);
            }
            playerInfo.endGroup();
            result = m;
        }
        break;

    default:
        result = playerInfo.value(s, "");
        break;
    }

    LOG_INFO() << "T:" << s << result;
    return result;
}

QVariant PlayerBuildInfo::value(DeviceInfo item, unsigned int match)
{
    QStringList result;
    int i = 0;
    while(PlayerInfoList[i].item != item)
        i++;
    QString s = PlayerInfoList[i].name;

    switch(item) {
    case UsbIdErrorList:
    {
        // go through all targets and find the one indicated by the usb id "target".
        // return list of matching players (since it could be more than one)
        QStringList targets = targetNames(true);
        for(int i = 0; i < targets.size(); i++) {
            QStringList usbids = playerInfo.value(targets.at(i) + "/usberror").toStringList();
            for(int j = 0; j < usbids.size(); j++) {
                if(usbids.at(j).toUInt(nullptr, 0) == match) {
                    result << targets.at(i);
                }
            }
        }
        break;
    }

    case UsbIdTargetList:
    {
        QStringList targets = targetNames(true);
        for(int i = 0; i < targets.size(); i++) {
            QStringList usbids = playerInfo.value(targets.at(i) + "/usbid").toStringList();
            for(int j = 0; j < usbids.size(); j++) {
                if(usbids.at(j).toUInt(nullptr, 0) == match) {
                    result << targets.at(i);
                }
            }
        }
        break;
    }

    default:
        break;
    }
    LOG_INFO() << "T:" << s << result;
    return result;
}

QVariant PlayerBuildInfo::value(SystemUrl item)
{
    // locate setting item in server info file
    int i = 0;
    while(PlayerSystemUrls[i].item != item)
        i++;

    QVariant result = playerInfo.value(PlayerSystemUrls[i].name);
    LOG_INFO() << "U:" << PlayerSystemUrls[i].name << result;
    return result;
}


QString PlayerBuildInfo::statusAsString(QString platform)
{
    QString result;
    switch(value(BuildStatus, platform).toInt())
    {
    case STATUS_RETIRED:
        result = tr("Stable (Retired)");
        break;
    case STATUS_UNUSABLE:
        result = tr("Unusable");
        break;
    case STATUS_UNSTABLE:
        result = tr("Unstable");
        break;
    case STATUS_STABLE:
        result = tr("Stable");
        break;
    default:
        result = tr("Unknown");
        break;
    }

    return result;
}


QStringList PlayerBuildInfo::targetNames(bool all)
{
    QStringList result;
    playerInfo.beginGroup("platforms");
    QStringList a = playerInfo.childKeys();
    playerInfo.endGroup();
    for(int i = 0; i < a.size(); i++)
    {
        QString target = playerInfo.value("platforms/" + a.at(i), "null").toString();
        if(playerInfo.value(target + "/status").toString() != "disabled" || all) {
            result.append(target);
        }
    }
    return result;
}

