/* main.cpp - A Qt dialog for PIN entry.
 * Copyright (C) 2002, 2008 Klarälvdalens Datakonsult AB (KDAB)
 * Copyright (C) 2003, 2021 g10 Code GmbH
 * Copyright 2007 Ingo Klöcker
 *
 * Written by Steffen Hansen <steffen@klaralvdalens-datakonsult.se>.
 * Modified by Marcus Brinkmann <marcus@g10code.de>.
 * Modified by Marc Mutz <marc@kdab.com>
 * Software engineering by Ingo Klöcker <dev@ingo-kloecker.de>
 *
 * 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 (at your option) 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses/>.
 * SPDX-License-Identifier: GPL-2.0+
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "accessibility.h"
#include "pinentryconfirm.h"
#include "pinentrydialog.h"
#include "pinentry.h"
#include "util.h"

#include <QApplication>
#include <QDebug>
#include <QIcon>
#include <QMessageBox>
#include <QPushButton>
#include <QString>
#include <QWidget>
#if QT_VERSION >= 0x050000
#include <QWindow>
#endif

#include <stdio.h>
#include <errno.h>

#include <stdexcept>
#include <gpg-error.h>

#ifdef FALLBACK_CURSES
#include <pinentry-curses.h>
#endif

#if QT_VERSION >= 0x050000 && defined(QT_STATIC)
  #include <QtPlugin>
  #ifdef Q_OS_WIN
    #include <windows.h>
    #include <shlobj.h>
    Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin)
  #elif defined(Q_OS_MAC)
    Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin)
  #else
    Q_IMPORT_PLUGIN(QXcbIntegrationPlugin)
  #endif
#endif

#ifdef Q_OS_WIN
#include <windows.h>
#endif

#include "pinentry_debug.h"

static QString escape_accel(const QString &s)
{

    QString result;
    result.reserve(s.size());

    bool afterUnderscore = false;

    for (unsigned int i = 0, end = s.size() ; i != end ; ++i) {
        const QChar ch = s[i];
        if (ch == QLatin1Char('_')) {
            if (afterUnderscore) { // escaped _
                result += QLatin1Char('_');
                afterUnderscore = false;
            } else { // accel
                afterUnderscore = true;
            }
        } else {
            if (afterUnderscore ||  // accel
                    ch == QLatin1Char('&')) {  // escape & from being interpreted by Qt
                result += QLatin1Char('&');
            }
            result += ch;
            afterUnderscore = false;
        }
    }

    if (afterUnderscore)
        // trailing single underscore: shouldn't happen, but deal with it robustly:
    {
        result += QLatin1Char('_');
    }

    return result;
}

namespace
{
class InvalidUtf8 : public std::invalid_argument
{
public:
    InvalidUtf8() : std::invalid_argument("invalid utf8") {}
    ~InvalidUtf8() throw() {}
};
}

static const bool GPG_AGENT_IS_PORTED_TO_ONLY_SEND_UTF8 = false;

static QString from_utf8(const char *s)
{
    const QString result = QString::fromUtf8(s);
    if (result.contains(QChar::ReplacementCharacter)) {
        if (GPG_AGENT_IS_PORTED_TO_ONLY_SEND_UTF8) {
            throw InvalidUtf8();
        } else {
            return QString::fromLocal8Bit(s);
        }
    }

    return result;
}

static void
setup_foreground_window(QWidget *widget, WId parentWid)
{
#if QT_VERSION >= 0x050000
    /* For windows set the desktop window as the transient parent */
    QWindow *parentWindow = nullptr;
    if (parentWid) {
        parentWindow = QWindow::fromWinId(parentWid);
    }
#ifdef Q_OS_WIN
    if (!parentWindow) {
        HWND desktop = GetDesktopWindow();
        if (desktop) {
            parentWindow = QWindow::fromWinId((WId) desktop);
        }
    }
#endif
    if (parentWindow) {
        // Ensure that we have a native wid
        widget->winId();
        QWindow *wndHandle = widget->windowHandle();

        if (wndHandle) {
            wndHandle->setTransientParent(parentWindow);
        }
    }
#endif
    widget->setWindowFlags(Qt::Window |
                           Qt::CustomizeWindowHint |
                           Qt::WindowTitleHint |
                           Qt::WindowCloseButtonHint |
                           Qt::WindowStaysOnTopHint |
                           Qt::WindowMinimizeButtonHint);
}

static int
qt_cmd_handler(pinentry_t pe)
{
    int want_pass = !!pe->pin;

    const QString ok =
        pe->ok             ? escape_accel(from_utf8(pe->ok)) :
        pe->default_ok     ? escape_accel(from_utf8(pe->default_ok)) :
        /* else */           QLatin1String("&OK") ;
    const QString cancel =
        pe->cancel         ? escape_accel(from_utf8(pe->cancel)) :
        pe->default_cancel ? escape_accel(from_utf8(pe->default_cancel)) :
        /* else */           QLatin1String("&Cancel") ;

    unique_malloced_ptr<char> str{pinentry_get_title(pe)};
    const QString title =
        str       ? from_utf8(str.get()) :
        /* else */  QLatin1String("pinentry-qt") ;

    const QString repeatError =
        pe->repeat_error_string ? from_utf8(pe->repeat_error_string) :
                                  QLatin1String("Passphrases do not match");
    const QString repeatString =
        pe->repeat_passphrase ? from_utf8(pe->repeat_passphrase) :
                                QString();
    const QString visibilityTT =
        pe->default_tt_visi ? from_utf8(pe->default_tt_visi) :
                              QLatin1String("Show passphrase");
    const QString hideTT =
        pe->default_tt_hide ? from_utf8(pe->default_tt_hide) :
                              QLatin1String("Hide passphrase");

    const QString capsLockHint =
        pe->default_capshint ? from_utf8(pe->default_capshint) :
                              QLatin1String("Caps Lock is on");

    const QString generateLbl = pe->genpin_label ? from_utf8(pe->genpin_label) :
                                QString();
    const QString generateTT = pe->genpin_tt ? from_utf8(pe->genpin_tt) :
                               QString();


    if (want_pass) {
        PinEntryDialog pinentry(nullptr, 0, pe->timeout, true, !!pe->quality_bar,
                                repeatString, visibilityTT, hideTT);
        setup_foreground_window(&pinentry, pe->parent_wid);
        pinentry.setPinentryInfo(pe);
        pinentry.setPrompt(escape_accel(from_utf8(pe->prompt)));
        pinentry.setDescription(from_utf8(pe->description));
        pinentry.setRepeatErrorText(repeatError);
        pinentry.setGenpinLabel(generateLbl);
        pinentry.setGenpinTT(generateTT);
        pinentry.setCapsLockHint(capsLockHint);
        pinentry.setFormattedPassphrase({
            bool(pe->formatted_passphrase),
            from_utf8(pe->formatted_passphrase_hint)});
        pinentry.setConstraintsOptions({
            bool(pe->constraints_enforce),
            from_utf8(pe->constraints_hint_short),
            from_utf8(pe->constraints_hint_long),
            from_utf8(pe->constraints_error_title)
        });

        if (!title.isEmpty()) {
            pinentry.setWindowTitle(title);
        }

        /* If we reuse the same dialog window.  */
        pinentry.setPin(QString());

        pinentry.setOkText(ok);
        pinentry.setCancelText(cancel);
        if (pe->error) {
            pinentry.setError(from_utf8(pe->error));
        }
        if (pe->quality_bar) {
            pinentry.setQualityBar(from_utf8(pe->quality_bar));
        }
        if (pe->quality_bar_tt) {
            pinentry.setQualityBarTT(from_utf8(pe->quality_bar_tt));
        }
        bool ret = pinentry.exec();
        if (!ret) {
            if (pinentry.timedOut())
                pe->specific_err = gpg_error (GPG_ERR_TIMEOUT);
            return -1;
        }

        const QString pinStr = pinentry.pin();
        QByteArray pin = pinStr.toUtf8();

        if (!!pe->repeat_passphrase) {
            /* Should not have been possible to accept
               the dialog in that case but we do a safety
               check here */
            pe->repeat_okay = (pinStr == pinentry.repeatedPin());
        }

        int len = strlen(pin.constData());
        if (len >= 0) {
            pinentry_setbufferlen(pe, len + 1);
            if (pe->pin) {
                strcpy(pe->pin, pin.constData());
                return len;
            }
        }
        return -1;
    } else {
        const QString desc  = pe->description ? from_utf8(pe->description) : QString();
        const QString notok = pe->notok       ? escape_accel(from_utf8(pe->notok)) : QString();

        const QMessageBox::StandardButtons buttons =
            pe->one_button ? QMessageBox::Ok :
            pe->notok      ? QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel :
            /* else */       QMessageBox::Ok | QMessageBox::Cancel ;

        PinentryConfirm box{QMessageBox::Information, title, desc, buttons};
        box.setTextFormat(Qt::PlainText);
        box.setTextInteractionFlags(Qt::TextSelectableByMouse);
        box.setTimeout(std::chrono::seconds{pe->timeout});
        setup_foreground_window(&box, pe->parent_wid);

        const struct {
            QMessageBox::StandardButton button;
            QString label;
        } buttonLabels[] = {
            { QMessageBox::Ok,     ok     },
            { QMessageBox::Yes,    ok     },
            { QMessageBox::No,     notok  },
            { QMessageBox::Cancel, cancel },
        };

        for (size_t i = 0 ; i < sizeof buttonLabels / sizeof * buttonLabels ; ++i)
            if ((buttons & buttonLabels[i].button) && !buttonLabels[i].label.isEmpty()) {
                box.button(buttonLabels[i].button)->setText(buttonLabels[i].label);
                Accessibility::setDescription(box.button(buttonLabels[i].button),
                                              buttonLabels[i].label);
            }

        box.setIconPixmap(applicationIconPixmap());

        if (!pe->one_button) {
            box.setDefaultButton(QMessageBox::Cancel);
        }

        box.show();
        raiseWindow(&box);

        const int rc = box.exec();

        if (rc == QMessageBox::Cancel) {
            pe->canceled = true;
        }
        if (box.timedOut()) {
          pe->specific_err = gpg_error (GPG_ERR_TIMEOUT);
        }

        return rc == QMessageBox::Ok || rc == QMessageBox::Yes ;

    }
}

static int
qt_cmd_handler_ex(pinentry_t pe)
{
    try {
        return qt_cmd_handler(pe);
    } catch (const InvalidUtf8 &) {
        pe->locale_err = true;
        return pe->pin ? -1 : false ;
    } catch (...) {
        pe->canceled = true;
        return pe->pin ? -1 : false ;
    }
}

pinentry_cmd_handler_t pinentry_cmd_handler = qt_cmd_handler_ex;

int
main(int argc, char *argv[])
{
    pinentry_init("pinentry-qt");

    QApplication *app = NULL;
    int new_argc = 0;

#ifdef FALLBACK_CURSES
#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
    // check a few environment variables that are usually set on X11 or Wayland sessions
    const bool hasWaylandDisplay = qEnvironmentVariableIsSet("WAYLAND_DISPLAY");
    const bool isWaylandSessionType = qgetenv("XDG_SESSION_TYPE") == "wayland";
    const bool hasX11Display = pinentry_have_display(argc, argv);
    const bool isX11SessionType = qgetenv("XDG_SESSION_TYPE") == "x11";
    const bool isGUISession = hasWaylandDisplay || isWaylandSessionType || hasX11Display || isX11SessionType;
    qCDebug(PINENTRY_LOG) << "hasWaylandDisplay:" << hasWaylandDisplay;
    qCDebug(PINENTRY_LOG) << "isWaylandSessionType:" << isWaylandSessionType;
    qCDebug(PINENTRY_LOG) << "hasX11Display:" << hasX11Display;
    qCDebug(PINENTRY_LOG) << "isX11SessionType:" << isX11SessionType;
    qCDebug(PINENTRY_LOG) << "isGUISession:" << isGUISession;
#else
    const bool isGUISession = pinentry_have_display(argc, argv);
#endif
    if (!isGUISession) {
        pinentry_cmd_handler = curses_cmd_handler;
        pinentry_set_flavor_flag ("curses");
    } else
#endif
    {
        /* Qt does only understand -display but not --display; thus we
           are fixing that here.  The code is pretty simply and may get
           confused if an argument is called "--display". */
        char **new_argv, *p;
        size_t n;
        int i, done;

        for (n = 0, i = 0; i < argc; i++) {
            n += strlen(argv[i]) + 1;
        }
        n++;
        new_argv = (char **)calloc(argc + 1, sizeof * new_argv);
        if (new_argv) {
            *new_argv = (char *)malloc(n);
        }
        if (!new_argv || !*new_argv) {
            fprintf(stderr, "pinentry-qt: can't fixup argument list: %s\n",
                    strerror(errno));
            exit(EXIT_FAILURE);

        }
        for (done = 0, p = *new_argv, i = 0; i < argc; i++)
            if (!done && !strcmp(argv[i], "--display")) {
                new_argv[i] = strcpy(p, argv[i] + 1);
                p += strlen(argv[i] + 1) + 1;
                done = 1;
            } else {
                new_argv[i] = strcpy(p, argv[i]);
                p += strlen(argv[i]) + 1;
            }

        /* Note: QApplication uses int &argc so argc has to be valid
         * for the full lifetime of the application.
         *
         * As Qt might modify argc / argv we use copies here so that
         * we do not loose options that are handled in both. e.g. display.
         */
        new_argc = argc;
        Q_ASSERT (new_argc);
        app = new QApplication(new_argc, new_argv);
        app->setWindowIcon(QIcon(QLatin1String(":/icons/document-encrypt.png")));
    }

    pinentry_parse_opts(argc, argv);

    int rc = pinentry_loop();
    delete app;
    return rc ? EXIT_FAILURE : EXIT_SUCCESS ;
}
