/***************************************************************************
 * SPDX-FileCopyrightText: 2015 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * 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, write to the
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 ***************************************************************************/

#include <QDebug>

#include "VirtualTerminal.h"

#include <QFileInfo>
#include <errno.h>
#include <fcntl.h>
#include <linux/kd.h>
#include <linux/vt.h>
#include <qscopeguard.h>
#include <signal.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define RELEASE_DISPLAY_SIGNAL (SIGRTMAX)
#define ACQUIRE_DISPLAY_SIGNAL (SIGRTMAX - 1)

namespace PLASMALOGIN
{
namespace VirtualTerminal
{
const char *defaultVtPath = "/dev/tty0";

QString path(int vt)
{
    return QStringLiteral("/dev/tty%1").arg(vt);
}

int getVtActive(int fd)
{
    vt_stat vtState{};
    if (ioctl(fd, VT_GETSTATE, &vtState) < 0) {
        qCritical() << "Failed to get current VT:" << strerror(errno);
        return -1;
    }
    return vtState.v_active;
}

static void onAcquireDisplay([[maybe_unused]] int signal)
{
    int fd = open(defaultVtPath, O_RDWR | O_NOCTTY);
    ioctl(fd, VT_RELDISP, VT_ACKACQ);
    close(fd);
}

static void onReleaseDisplay([[maybe_unused]] int signal)
{
    int fd = open(defaultVtPath, O_RDWR | O_NOCTTY);
    ioctl(fd, VT_RELDISP, 1);
    close(fd);
}

static bool handleVtSwitches(int fd)
{
    vt_mode setModeRequest{};
    bool ok = true;

    setModeRequest.mode = VT_PROCESS;
    setModeRequest.relsig = RELEASE_DISPLAY_SIGNAL;
    setModeRequest.acqsig = ACQUIRE_DISPLAY_SIGNAL;

    if (ioctl(fd, VT_SETMODE, &setModeRequest) < 0) {
        qDebug() << "Failed to manage VT manually:" << strerror(errno);
        ok = false;
    }

    signal(RELEASE_DISPLAY_SIGNAL, onReleaseDisplay);
    signal(ACQUIRE_DISPLAY_SIGNAL, onAcquireDisplay);

    return ok;
}

static void fixVtMode(int fd, bool vt_auto)
{
    vt_mode getmodeReply{};
    int kernelDisplayMode = 0;
    bool modeFixed = false;
    bool ok = true;

    if (ioctl(fd, VT_GETMODE, &getmodeReply) < 0) {
        qWarning() << "Failed to query VT mode:" << strerror(errno);
        ok = false;
    }

    if (getmodeReply.mode != VT_AUTO) {
        goto out;
    }

    if (ioctl(fd, KDGETMODE, &kernelDisplayMode) < 0) {
        qWarning() << "Failed to query kernel display mode:" << strerror(errno);
        ok = false;
    }

    if (kernelDisplayMode == KD_TEXT) {
        goto out;
    }

    // VT is in the VT_AUTO + KD_GRAPHICS state, fix it
    if (vt_auto) {
        // If vt_auto is true, the controlling process is already gone, so there is no
        // process which could send the VT_RELDISP 1 ioctl to release the vt.
        // Switch to KD_TEXT and let the kernel switch vts automatically
        if (ioctl(fd, KDSETMODE, KD_TEXT) < 0) {
            qWarning("Failed to set text mode for current VT: %s", strerror(errno));
            ok = false;
        }
    } else {
        ok = handleVtSwitches(fd);
        modeFixed = true;
    }
out:
    if (!ok) {
        qCritical() << "Failed to set up VT mode";
        return;
    }

    if (modeFixed) {
        qDebug() << "VT mode fixed";
    } else {
        qDebug() << "VT mode didn't need to be fixed";
    }
}

int currentVt()
{
    int fd = open(defaultVtPath, O_RDWR | O_NOCTTY);
    if (fd < 0) {
        qCritical() << "Failed to open VT master:" << strerror(errno);
        return -1;
    }
    auto closeFd = qScopeGuard([fd] {
        close(fd);
    });

    return getVtActive(fd);
}

int setUpNewVt()
{
    // open VT master
    int fd = open(defaultVtPath, O_RDWR | O_NOCTTY);
    if (fd < 0) {
        qCritical() << "Failed to open VT master:" << strerror(errno);
        return -1;
    }
    auto closeFd = qScopeGuard([fd] {
        close(fd);
    });

    int vt = 0;
    if (ioctl(fd, VT_OPENQRY, &vt) < 0) {
        qCritical() << "Failed to open new VT:" << strerror(errno);
        return -1;
    }

    // fallback to active VT
    if (vt <= 0) {
        int vtActive = getVtActive(fd);
        qWarning() << "New VT" << vt << "is not valid, fall back to" << vtActive;
        return vtActive;
    }

    return vt;
}

void jumpToVt(int vt, bool vt_auto)
{
    qDebug() << "Jumping to VT" << vt;

    int fd;

    int activeVtFd = open(defaultVtPath, O_RDWR | O_NOCTTY);

    QString ttyString = path(vt);
    int vtFd = open(qPrintable(ttyString), O_RDWR | O_NOCTTY);
    if (vtFd != -1) {
        fd = vtFd;

        // Clear VT
        static const char *clearEscapeSequence = "\33[H\33[2J";
        if (write(vtFd, clearEscapeSequence, sizeof(clearEscapeSequence)) == -1) {
            qWarning("Failed to clear VT %d: %s", vt, strerror(errno));
        }

        // set graphics mode to prevent flickering
        if (ioctl(fd, KDSETMODE, KD_GRAPHICS) < 0) {
            qWarning("Failed to set graphics mode for VT %d: %s", vt, strerror(errno));
        }

        // it's possible that the current VT was left in a broken
        // combination of states (KD_GRAPHICS with VT_AUTO) that we
        // cannot switch from, so make sure things are in a way that
        // will make VT_ACTIVATE work without hanging VT_WAITACTIVE
        fixVtMode(activeVtFd, vt_auto);
    } else {
        qWarning("Failed to open %s: %s", qPrintable(ttyString), strerror(errno));
        qDebug("Using %s instead of %s!", defaultVtPath, qPrintable(ttyString));
        fd = activeVtFd;
    }

    // If vt_auto is true, the controlling process is already gone, so there is no
    // process which could send the VT_RELDISP 1 ioctl to release the vt.
    // Let the kernel switch vts automatically
    if (!vt_auto) {
        handleVtSwitches(fd);
    }

    do {
        errno = 0;

        if (ioctl(fd, VT_ACTIVATE, vt) < 0) {
            if (errno == EINTR) {
                continue;
            }

            qWarning("Couldn't initiate jump to VT %d: %s", vt, strerror(errno));
            break;
        }

        if (ioctl(fd, VT_WAITACTIVE, vt) < 0 && errno != EINTR) {
            qWarning("Couldn't finalize jump to VT %d: %s", vt, strerror(errno));
        }

    } while (errno == EINTR);
    close(activeVtFd);
    if (vtFd != -1) {
        close(vtFd);
    }
}
}
}
