/*
 * Common Carla code
 * Copyright (C) 2011-2019 Filipe Coelho <falktx@falktx.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or any later version.
 *
 * This program 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 General Public License for more details.
 *
 * For a full copy of the GNU General Public License see the doc/GPL.txt file.
 */

#include "carla_shared.hpp"

//---------------------------------------------------------------------------------------------------------------------
// Imports (Global)

#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wconversion"
# pragma GCC diagnostic ignored "-Weffc++"
# pragma GCC diagnostic ignored "-Wsign-conversion"
#endif

#include <QtGui/QFontMetrics>

#include <QtWidgets/QFileDialog>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLineEdit>

#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))
# pragma GCC diagnostic pop
#endif

//---------------------------------------------------------------------------------------------------------------------

#ifdef CARLA_OS_UNIX
# include <signal.h>
#endif

//---------------------------------------------------------------------------------------------------------------------
// Imports (Custom)

#include "carla_host.hpp"

#include "CarlaUtils.h"
#include "CarlaMathUtils.hpp"

//---------------------------------------------------------------------------------------------------------------------
// Global Carla object

CarlaObject::CarlaObject() noexcept
    : host(nullptr),
      gui(nullptr),
      nogui(false),
      term(false) {}

CarlaObject gCarla;

//---------------------------------------------------------------------------------------------------------------------
// Get Icon from user theme, using our own as backup (Oxygen)

QIcon getIcon(const QString icon, const int size)
{
    return QIcon::fromTheme(icon, QIcon(QString(":/%1x%1/%2.png").arg(size).arg(icon)));
}

//---------------------------------------------------------------------------------------------------------------------
// Handle some basic command-line arguments shared between all carla variants

QString handleInitialCommandLineArguments(const int argc, char* argv[])
{
    static const QStringList listArgsNoGUI   = { "-n", "--n", "-no-gui", "--no-gui", "-nogui", "--nogui" };
    static const QStringList listArgsHelp    = { "-h", "--h", "-help", "--help" };
    static const QStringList listArgsVersion = { "-v", "--v", "-version", "--version" };

    QString initName(argv[0]); // = os.path.basename(file) if (file is not None and os.path.dirname(file) in PATH) else sys.argv[0]
    // libPrefix = None

    for (int i=1; i<argc; ++i)
    {
        const QString arg(argv[i]);

        if (arg.startsWith("--with-appname="))
        {
            // initName = os.path.basename(arg.replace("--with-appname=", ""));
        }
        else if (arg.startsWith("--with-libprefix=") || arg == "--gdb")
        {
            pass();
        }
        else if (listArgsNoGUI.contains(arg))
        {
            gCarla.nogui = true;
        }
        else if (listArgsHelp.contains(arg))
        {
            carla_stdout("Usage: %s [OPTION]... [FILE|URL]", initName);
            carla_stdout("");
            carla_stdout(" where FILE can be a Carla project or preset file to be loaded, or URL if using Carla-Control");
            carla_stdout("");
            carla_stdout(" and OPTION can be one or more of the following:");
            carla_stdout("");
            carla_stdout("    --gdb    \t Run Carla inside gdb.");
            carla_stdout(" -n,--no-gui \t Run Carla headless, don't show UI.");
            carla_stdout("");
            carla_stdout(" -h,--help   \t Print this help text and exit.");
            carla_stdout(" -v,--version\t Print version information and exit.");
            carla_stdout("");

            std::exit(0);
        }
        else if (listArgsVersion.contains(arg))
        {
            /*
            QString pathBinaries, pathResources = getPaths();
            */

            carla_stdout("Using Carla version %s", CARLA_VERSION_STRING);
            /*
            carla_stdout("  Qt version:     %s", QT_VERSION_STR);
            carla_stdout("  Binary dir:     %s", pathBinaries.toUtf8());
            carla_stdout("  Resources dir:  %s", pathResources.toUtf8());
            */

            std::exit(0);
        }
    }

    return initName;
}

//---------------------------------------------------------------------------------------------------------------------
// Get initial project file (as passed in the command-line parameters)

QString getInitialProjectFile(bool)
{
    // TODO
    return "";
}

//---------------------------------------------------------------------------------------------------------------------
// Get paths (binaries, resources)

bool getPaths(QString& pathBinaries, QString& pathResources)
{
    const QString libFolder(carla_get_library_folder());

    QDir dir(libFolder);

    // FIXME need to completely rework this in C++ mode, so check if all cases are valid

    if (libFolder.endsWith("bin"))
    {
        CARLA_SAFE_ASSERT_RETURN(dir.cd("resources"), false);
        pathBinaries  = libFolder;
        pathResources = dir.absolutePath();
        return true;
    }
    else if (libFolder.endsWith("carla"))
    {
        for (int i=2; --i>=0;)
        {
            CARLA_SAFE_ASSERT_INT_RETURN(dir.cdUp(), i, false);
            CARLA_SAFE_ASSERT_INT_RETURN(dir.cd("share"), i, false);

            if (dir.exists())
            {
                CARLA_SAFE_ASSERT_INT_RETURN(dir.cd("carla"), i, false);
                CARLA_SAFE_ASSERT_INT_RETURN(dir.cd("resources"), i, false);
                pathBinaries  = libFolder;
                pathResources = dir.absolutePath();
                return true;
            }
        }
    }

    return false;
}

//---------------------------------------------------------------------------------------------------------------------
// Signal handler

static void signalHandler(const int sig)
{
    switch (sig)
    {
    case SIGINT:
    case SIGTERM:
        gCarla.term = true;
        if (gCarla.host != nullptr)
            emit gCarla.host->SignalTerminate();
        break;
    case SIGUSR1:
        if (gCarla.host != nullptr)
            emit gCarla.host->SignalSave();
        break;
    }
}

#ifdef CARLA_OS_WIN
static BOOL WINAPI winSignalHandler(DWORD dwCtrlType) noexcept
{
    if (dwCtrlType == CTRL_C_EVENT)
    {
        signalHandler(SIGINT);
        return TRUE;
    }
    return FALSE;
}
#endif

void setUpSignals()
{
#if defined(CARLA_OS_UNIX)
    struct sigaction sig;
    carla_zeroStruct(sig);
    sig.sa_handler = signalHandler;
    sig.sa_flags   = SA_RESTART;
    sigemptyset(&sig.sa_mask);
    sigaction(SIGTERM, &sig, nullptr);
    sigaction(SIGINT, &sig, nullptr);
    sigaction(SIGUSR1, &sig, nullptr);
#elif defined(CARLA_OS_WIN)
    SetConsoleCtrlHandler(winSignalHandler, TRUE);
#endif
}

//---------------------------------------------------------------------------------------------------------------------
// QLineEdit and QPushButton combo

QString getAndSetPath(QWidget* const parent, QLineEdit* const lineEdit)
{
    const QCarlaString newPath = QFileDialog::getExistingDirectory(parent, parent->tr("Set Path"), lineEdit->text(), QFileDialog::ShowDirsOnly);
    if (newPath.isNotEmpty())
        lineEdit->setText(newPath);
    return newPath;
}

//---------------------------------------------------------------------------------------------------------------------
// fill up a qlists from a C arrays

void fillQStringListFromStringArray(QStringList& list, const char* const* const stringArray)
{
    int count = 0;

    // count number of strings first
    for (; stringArray[count] != nullptr; ++count) {}

    // allocate list
    list.reserve(count);

    // fill in strings
    for (count = 0; stringArray[count] != nullptr; ++count)
        list.append(stringArray[count]);
}

void fillQDoubleListFromDoubleArray(QList<double>& list, const double* const doubleArray)
{
    int count = 0;

    // count number of strings first
    for (; carla_isNotZero(doubleArray[count]); ++count) {}

    // allocate list
    list.reserve(count);

    // fill in strings
    for (count = 0; carla_isNotZero(doubleArray[count]); ++count)
        list.append(doubleArray[count]);
}

void fillQUIntListFromUIntArray(QList<uint>& list, const uint* const uintArray)
{
    int count = 0;

    // count number of strings first
    for (; uintArray[count] != 0; ++count) {}

    // allocate list
    list.reserve(count);

    // fill in strings
    for (count = 0; uintArray[count] != 0; ++count)
        list.append(uintArray[count]);
}

//---------------------------------------------------------------------------------------------------------------------
// Backwards-compatible horizontalAdvance/width call, depending on Qt version

int fontMetricsHorizontalAdvance(const QFontMetrics& fm, const QString& s)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
    return fm.horizontalAdvance(s);
#else
    return fm.width(s);
#endif
}

//---------------------------------------------------------------------------------------------------------------------
// Check if a string array contains a string

bool stringArrayContainsString(const char* const* const stringArray, const char* const string) noexcept
{
    for (uint i=0; stringArray[i] != nullptr; ++i)
    {
        if (std::strcmp(stringArray[i], string) == 0)
            return true;
    }

    return false;
}

//---------------------------------------------------------------------------------------------------------------------
// Get index of a QList<double> value

int getIndexOfQDoubleListValue(const QList<double>& list, const double value)
{
    if (list.size() > 0)
    {
        for (QList<double>::const_iterator n = list.cbegin(), e = list.cend(); n != e; ++n)
            if (carla_isEqual(*n, value))
                return int(n - list.cbegin());
    }

    return -1;
}

//---------------------------------------------------------------------------------------------------------------------
// Check if two QList<double> instances match

bool isQDoubleListEqual(const QList<double>& list1, const QList<double>& list2)
{
    if (list1.size() != list2.size())
        return false;
    if (list1.isEmpty())
        return true;

    for (QList<double>::const_iterator l1n = list1.cbegin(), l2n = list2.cbegin(), l1e = list1.cend(); l1n != l1e; ++l1n, ++l2n)
        if (carla_isNotEqual(*l1n, *l2n))
            return false;

    return true;
}

//---------------------------------------------------------------------------------------------------------------------
// Custom QMessageBox which resizes itself to fit text

void QMessageBoxWithBetterWidth::showEvent(QShowEvent* const event)
{
    const QFontMetrics metrics(fontMetrics());
    const QStringList lines(text().trimmed().split("\n") + informativeText().trimmed().split("\n"));

    if (lines.size() > 0)
    {
        int width = 0;

        for (const QString& line : lines)
            width = std::max(fontMetricsHorizontalAdvance(metrics, line), width);

        if (QGridLayout* const layout_ = dynamic_cast<QGridLayout*>(layout()))
            layout_->setColumnMinimumWidth(2, width + 12);
    }

    QMessageBox::showEvent(event);
}

//---------------------------------------------------------------------------------------------------------------------
// Safer QSettings class, which does not throw if type mismatches

bool QSafeSettings::valueBool(const QString key, const bool defaultValue) const
{
    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::Bool), defaultValue);

    return var.isValid() ? var.toBool() : defaultValue;
}

Qt::CheckState QSafeSettings::valueCheckState(const QString key, const Qt::CheckState defaultValue) const
{
    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::UInt), defaultValue);

    if (! var.isValid())
        return defaultValue;

    const uint value = var.toUInt();

    switch (value)
    {
    case Qt::Unchecked:
    case Qt::PartiallyChecked:
    case Qt::Checked:
        return static_cast<Qt::CheckState>(value);
    default:
        return defaultValue;
    }
}

int QSafeSettings::valueIntPositive(const QString key, const int defaultValue) const
{
    CARLA_SAFE_ASSERT_INT(defaultValue >= 0, defaultValue);

    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::Int), defaultValue);
    CARLA_SAFE_ASSERT_RETURN(var.isValid(), defaultValue);

    const int value = var.toInt();
    CARLA_SAFE_ASSERT_RETURN(value >= 0, defaultValue);

    return value;
}

uint QSafeSettings::valueUInt(const QString key, const uint defaultValue) const
{
    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::UInt), defaultValue);

    return var.isValid() ? var.toUInt() : defaultValue;
}

double QSafeSettings::valueDouble(const QString key, const double defaultValue) const
{
    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::Double), defaultValue);

    return var.isValid() ? var.toDouble() : defaultValue;
}

QString QSafeSettings::valueString(const QString key, const QString defaultValue) const
{
    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::String), defaultValue);

    return var.isValid() ? var.toString() : defaultValue;
}

QByteArray QSafeSettings::valueByteArray(const QString key, const QByteArray defaultValue) const
{
    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::ByteArray), defaultValue);

    return var.isValid() ? var.toByteArray() : defaultValue;
}

QStringList QSafeSettings::valueStringList(const QString key, const QStringList defaultValue) const
{
    QVariant var(value(key, defaultValue));

    if (var.isNull())
        return defaultValue;

    CARLA_SAFE_ASSERT_RETURN(var.convert(QVariant::StringList), defaultValue);

    return var.isValid() ? var.toStringList() : defaultValue;
}

//---------------------------------------------------------------------------------------------------------------------
// Custom MessageBox

int CustomMessageBox(QWidget* const parent,
                     const QMessageBox::Icon icon,
                     const QString title,
                     const QString text,
                     const QString extraText,
                     const QMessageBox::StandardButtons buttons,
                     const QMessageBox::StandardButton defButton)
{
    QMessageBoxWithBetterWidth msgBox(parent);
    msgBox.setIcon(icon);
    msgBox.setWindowTitle(title);
    msgBox.setText(text);
    msgBox.setInformativeText(extraText);
    msgBox.setStandardButtons(buttons);
    msgBox.setDefaultButton(defButton);
    return msgBox.exec();
}

//---------------------------------------------------------------------------------------------------------------------
