#include <exception>
#include <iostream>
#include <stdexcept>
#include <string>

#include <fftw3.h>
#include <locale.h>

#include <QApplication>
#include <QDateTime>
#include <QDir>
#include <QLibraryInfo>
#include <QLockFile>
#include <QLoggingCategory>
#include <QObject>
#include <QRegularExpression>
#include <QSettings>
#include <QStack>
#include <QStandardPaths>
#include <QStringList>
#include <QSysInfo>

#if QT_VERSION >= 0x050200
#include <QCommandLineOption>
#include <QCommandLineParser>
#endif

#include "FrequencyList.h"
#include "JS8MessageBox.h"
#include "JS8_Include/SettingsGroup.h"
#include "JS8_Include/commons.h"
#include "JS8_UI/mainwindow.h"
#include "MetaDataRegistry.h"
#include "MultiSettings.h"
#include "Radio.h"
#include "TraceFile.h"
#include "revision_utils.h"

#include "DriftingDateTime.h"

Q_DECLARE_LOGGING_CATEGORY(main_js8)

namespace {
class MessageTimestamper {
  public:
    MessageTimestamper() {
        prior_handlers_.push(qInstallMessageHandler(message_handler));
    }
    ~MessageTimestamper() {
        if (prior_handlers_.size())
            qInstallMessageHandler(prior_handlers_.pop());
    }

  private:
    static void message_handler(QtMsgType type,
                                QMessageLogContext const &context,
                                QString const &msg) {
        QtMessageHandler handler{prior_handlers_.top()};
        if (handler) {
            handler(type, context,
                    DriftingDateTime::currentDateTimeUtc().toString(
                        "yy-MM-ddTHH:mm:ss.zzzZ: ") +
                        msg);
        }
    }
    static QStack<QtMessageHandler> prior_handlers_;
};
QStack<QtMessageHandler> MessageTimestamper::prior_handlers_;
} // namespace

int main(int argc, char *argv[]) {
    // Add timestamps to all debug messages
    MessageTimestamper message_timestamper;

    // make the Qt type magic happen
    Radio::register_types();
    register_types();

    QApplication a(argc, argv);
    try {
        setlocale(LC_NUMERIC, "C"); // ensure number forms are in
                                    // consistent format, do this after
                                    // instantiating QApplication so
                                    // that GUI has correct l18n

        // Override programs executable basename as application name.
        a.setApplicationName("JS8Call");
        a.setApplicationVersion(version());

#if QT_VERSION >= 0x050200
        QCommandLineParser parser;
        parser.setApplicationDescription("\n" PROJECT_SUMMARY_DESCRIPTION);
        auto help_option = parser.addHelpOption();
        auto version_option = parser.addVersionOption();

        QCommandLineOption output_option(QStringList() << "o" << "output",
                                         "Write debug statements into <file>.",
                                         "file");
        parser.addOption(output_option);

        // support for multiple instances running from a single installation
        QCommandLineOption rig_option(
            QStringList{} << "r" << "rig-name",
            a.translate("main",
                        "Where <rig-name> is for multi-instance support."),
            a.translate("main", "rig-name"));
        parser.addOption(rig_option);

        // support for start up configuration
        QCommandLineOption cfg_option(
            QStringList{} << "c" << "config",
            a.translate("main", "Where <configuration> is an existing one."),
            a.translate("main", "configuration"));
        parser.addOption(cfg_option);

        QCommandLineOption test_option(
            QStringList{} << "test-mode",
            a.translate("main", "Writable files in test location.  Use with "
                                "caution, for testing only."));
        parser.addOption(test_option);

        if (!parser.parse(a.arguments())) {
            std::cerr << parser.errorText().toLocal8Bit().data() << std::endl;
            return -1;
        } else {
            if (parser.isSet(help_option)) {
                parser.showHelp(-1);
                return 0;
            } else if (parser.isSet(version_option)) {
                parser.showVersion();
                return 0;
            }
        }

        if (parser.isSet(output_option)) {
            new TraceFile(parser.value(output_option));
        }

        QStandardPaths::setTestModeEnabled(parser.isSet(test_option));

        // support for multiple instances running from a single installation
        bool multiple{false};
        if (parser.isSet(rig_option) || parser.isSet(test_option)) {
            auto temp_name = parser.value(rig_option);
            if (!temp_name.isEmpty()) {
                if (temp_name.contains(QRegularExpression{R"([\\/,])"})) {
                    std::cerr << "Invalid rig name - \\ & / not allowed"
                              << std::endl;
                    parser.showHelp(-1);
                }

                a.setApplicationName(a.applicationName() + " - " + temp_name);
            }

            if (parser.isSet(test_option)) {
                a.setApplicationName(a.applicationName() + " - test");
            }

            multiple = true;
        }

        // now we have the application name we can open the settings
        MultiSettings multi_settings{parser.value(cfg_option)};

        // find the temporary files path
        QDir temp_dir{
            QStandardPaths::writableLocation(QStandardPaths::TempLocation)};
        Q_ASSERT(temp_dir.exists()); // sanity check

        // disallow multiple instances with same instance key
        QLockFile instance_lock{
            temp_dir.absoluteFilePath(a.applicationName() + ".lock")};
        instance_lock.setStaleLockTime(0);
        while (!instance_lock.tryLock()) {
            if (QLockFile::LockFailedError == instance_lock.error()) {
                switch (JS8MessageBox::query_message(
                    nullptr,
                    a.translate("main", "Another instance may be running"),
                    a.translate("main", "try to remove stale lock file?"),
                    QString{},
                    JS8MessageBox::Yes | JS8MessageBox::Retry |
                        JS8MessageBox::No,
                    JS8MessageBox::Yes)) {
                case JS8MessageBox::Yes:
                    instance_lock.removeStaleLockFile();
                    break;

                case JS8MessageBox::Retry:
                    break;

                default:
                    throw std::runtime_error{
                        "Multiple instances must have unique rig names"};
                }
            } else {
                throw std::runtime_error{"Failed to access lock file"};
            }
        }
#endif

#if WSJT_QDEBUG_TO_FILE
        // Open a trace file
        TraceFile trace_file{
            temp_dir.absoluteFilePath(a.applicationName() + "_trace.log")};
        qCDebug(main_js8) << program_title() + " - Program startup";
#endif

        // Create a unique writeable temporary directory in a suitable location
        bool temp_ok{false};
        QString unique_directory{QApplication::applicationName()};
        do {
            if (!temp_dir.mkpath(unique_directory) ||
                !temp_dir.cd(unique_directory)) {
                JS8MessageBox::critical_message(
                    nullptr,
                    a.translate("main",
                                "Failed to create a temporary directory"),
                    a.translate("main", "Path: \"%1\"")
                        .arg(temp_dir.absolutePath()));
                throw std::runtime_error{
                    "Failed to create a temporary directory"};
            }
            if (!temp_dir.isReadable() ||
                !(temp_ok = QTemporaryFile{temp_dir.absoluteFilePath("test")}
                                .open())) {
                auto button = JS8MessageBox::critical_message(
                    nullptr,
                    a.translate(
                        "main",
                        "Failed to create a usable temporary directory"),
                    a.translate(
                        "main",
                        "Another application may be locking the directory"),
                    a.translate("main", "Path: \"%1\"")
                        .arg(temp_dir.absolutePath()),
                    JS8MessageBox::Retry | JS8MessageBox::Cancel);
                if (JS8MessageBox::Cancel == button) {
                    throw std::runtime_error{
                        "Failed to create a usable temporary directory"};
                }
                temp_dir.cdUp(); // revert to parent as this one is no good
            }
        } while (!temp_ok);

        int result;
        do {
#if WSJT_QDEBUG_TO_FILE
            // announce to trace file and dump settings
            qCDebug(main_js8) << "++++++++++++++++++++++++++++ Settings "
                                 "++++++++++++++++++++++++++++";
            for (auto const &key : multi_settings.settings()->allKeys()) {
                auto const &value = multi_settings.settings()->value(key);
                if (value.canConvert<QVariantList>()) {
                    auto const sequence = value.value<QSequentialIterable>();
                    qCDebug(main_js8).nospace() << key << ": ";
                    for (auto const &item : sequence) {
                        qCDebug(main_js8).nospace() << '\t' << item;
                    }
                } else {
                    qCDebug(main_js8).nospace() << key << ": " << value;
                }
            }
            qCDebug(main_js8) << "---------------------------- Settings "
                                 "----------------------------";
#endif

            // run the application UI
            MainWindow w(program_version(), temp_dir, multiple,
                         &multi_settings);
            w.show();
            result = a.exec();
        } while (!result && !multi_settings.exit());

        fftwf_forget_wisdom();
        fftwf_cleanup();

        temp_dir.removeRecursively(); // clean up temp files
        return result;
    } catch (std::exception const &e) {
        JS8MessageBox::critical_message(nullptr, "Fatal error", e.what());
        std::cerr << "Error: " << e.what() << '\n';
    } catch (...) {
        JS8MessageBox::critical_message(nullptr, "Unexpected fatal error");
        std::cerr << "Unexpected fatal error\n";
        throw; // hoping the runtime might tell us more about the exception
    }
    return -1;
}

Q_LOGGING_CATEGORY(main_js8, "main.js8", QtWarningMsg)
