// Copyright (C) 2017 The Qt Company Ltd.
// Copyright (C) 2015-2016 Oleksandr Tymoshenko <gonzo@bluezbox.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qbsdfbscreen.h"
#include <QtFbSupport/private/qfbcursor_p.h>
#include <QtFbSupport/private/qfbwindow_p.h>
#include <QtCore/QFile>
#include <QtCore/QRegularExpression>
#include <QtGui/QPainter>

#include <private/qcore_unix_p.h> // overrides QT_OPEN
#include <qimage.h>
#include <qdebug.h>

#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <limits.h>
#include <signal.h>

#include <sys/consio.h>
#include <sys/fbio.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

enum {
    DefaultDPI = 100
};

static int openFramebufferDevice(const QString &dev)
{
    const QByteArray devPath = QFile::encodeName(dev);

    int fd = QT_OPEN(devPath.constData(), O_RDWR);

    if (fd == -1)
        fd = QT_OPEN(devPath.constData(), O_RDONLY);

    return fd;
}

static QRect determineGeometry(const struct fbtype &fb, const QRect &userGeometry)
{
    int xoff = 0;
    int yoff = 0;
    int w = 0;
    int h = 0;

    if (userGeometry.isValid()) {
        w = qMin(userGeometry.width(), fb.fb_width);
        h = qMin(userGeometry.height(), fb.fb_height);

        int xxoff = userGeometry.x(), yyoff = userGeometry.y();
        if (xxoff != 0 || yyoff != 0) {
            if (xxoff < 0 || xxoff + w > fb.fb_width)
                xxoff = fb.fb_width - w;
            if (yyoff < 0 || yyoff + h > fb.fb_height)
                yyoff = fb.fb_height - h;
            xoff += xxoff;
            yoff += yyoff;
        } else {
            xoff += (fb.fb_width - w)/2;
            yoff += (fb.fb_height - h)/2;
        }
    } else {
        w = fb.fb_width;
        h = fb.fb_height;
    }

    if (w == 0 || h == 0) {
        qWarning("Unable to find screen geometry, using 320x240");
        w = 320;
        h = 240;
    }

    return QRect(xoff, yoff, w, h);
}

static QSizeF determinePhysicalSize(const QSize &mmSize, const QSize &res)
{
    int mmWidth = mmSize.width();
    int mmHeight = mmSize.height();

    if (mmWidth <= 0 && mmHeight <= 0) {
        const int dpi = DefaultDPI;
        mmWidth = qRound(res.width() * 25.4 / dpi);
        mmHeight = qRound(res.height() * 25.4 / dpi);
    } else if (mmWidth > 0 && mmHeight <= 0) {
        mmHeight = res.height() * mmWidth/res.width();
    } else if (mmHeight > 0 && mmWidth <= 0) {
        mmWidth = res.width() * mmHeight/res.height();
    }

    return QSize(mmWidth, mmHeight);
}

QBsdFbScreen::QBsdFbScreen(const QStringList &args)
    : m_arguments(args)
{
}

QBsdFbScreen::~QBsdFbScreen()
{
    if (m_framebufferFd != -1) {
        munmap(m_mmap.data - m_mmap.offset, m_mmap.size);
        qt_safe_close(m_framebufferFd);
    }
}

bool QBsdFbScreen::initialize()
{
    QRegularExpression fbRx("fb=(.*)"_L1);
    QRegularExpression mmSizeRx("mmsize=(\\d+)x(\\d+)"_L1);
    QRegularExpression sizeRx("size=(\\d+)x(\\d+)"_L1);
    QRegularExpression offsetRx("offset=(\\d+)x(\\d+)"_L1);

    QString fbDevice;
    QSize userMmSize;
    QRect userGeometry;

    // Parse arguments
    for (const QString &arg : std::as_const(m_arguments)) {
        QRegularExpressionMatch match;
        if (arg.contains(mmSizeRx, &match))
            userMmSize = QSize(match.captured(1).toInt(), match.captured(2).toInt());
        else if (arg.contains(sizeRx, &match))
            userGeometry.setSize(QSize(match.captured(1).toInt(), match.captured(2).toInt()));
        else if (arg.contains(offsetRx, &match))
            userGeometry.setTopLeft(QPoint(match.captured(1).toInt(), match.captured(2).toInt()));
        else if (arg.contains(fbRx, &match))
            fbDevice = match.captured(1);
    }

    if (!fbDevice.isEmpty()) {
        // Open the device
        m_framebufferFd = openFramebufferDevice(fbDevice);
    } else {
        m_framebufferFd = STDIN_FILENO;
    }

    if (m_framebufferFd == -1) {
        qErrnoWarning(errno, "Failed to open framebuffer %s", qPrintable(fbDevice));
        return false;
    }

    struct fbtype fb;
    if (ioctl(m_framebufferFd, FBIOGTYPE, &fb) != 0) {
        qErrnoWarning(errno, "Error reading framebuffer information");
        return false;
    }

    int line_length = 0;
    if (ioctl(m_framebufferFd, FBIO_GETLINEWIDTH, &line_length) != 0) {
        qErrnoWarning(errno, "Error reading line length information");
        return false;
    }

    mDepth = fb.fb_depth;

    m_bytesPerLine = line_length;
    const QRect geometry = determineGeometry(fb, userGeometry);
    mGeometry = QRect(QPoint(0, 0), geometry.size());
    switch (mDepth) {
    case 32:
        mFormat = QImage::Format_RGB32;
        break;
    case 24:
        mFormat = QImage::Format_RGB888;
        break;
    case 16:
        // falling back
    default:
        mFormat = QImage::Format_RGB16;
        break;
    }
    mPhysicalSize = determinePhysicalSize(userMmSize, geometry.size());

    // mmap the framebuffer
    const size_t pagemask = getpagesize() - 1;
    m_mmap.size = (m_bytesPerLine * fb.fb_height + pagemask) & ~pagemask;
    uchar *data = static_cast<uchar*>(mmap(nullptr, m_mmap.size, PROT_READ | PROT_WRITE, MAP_SHARED, m_framebufferFd, 0));
    if (data == MAP_FAILED) {
        qErrnoWarning(errno, "Failed to mmap framebuffer");
        return false;
    }

    m_mmap.offset = geometry.y() * m_bytesPerLine + geometry.x() * mDepth / 8;
    m_mmap.data = data + m_mmap.offset;

    QFbScreen::initializeCompositor();
    m_onscreenImage = QImage(m_mmap.data, geometry.width(), geometry.height(), m_bytesPerLine, mFormat);

    mCursor = new QFbCursor(this);

    return true;
}

QRegion QBsdFbScreen::doRedraw()
{
    const QRegion touched = QFbScreen::doRedraw();

    if (touched.isEmpty())
        return touched;

    if (!m_blitter)
        m_blitter.reset(new QPainter(&m_onscreenImage));

    for (const QRect &rect : touched)
        m_blitter->drawImage(rect, mScreenImage, rect);
    return touched;
}

// grabWindow() grabs "from the screen" not from the backingstores.
QPixmap QBsdFbScreen::grabWindow(WId wid, int x, int y, int width, int height) const
{
    if (!wid) {
        if (width < 0)
            width = m_onscreenImage.width() - x;
        if (height < 0)
            height = m_onscreenImage.height() - y;
        return QPixmap::fromImage(m_onscreenImage).copy(x, y, width, height);
    }

    const QFbWindow *window = windowForId(wid);
    if (window) {
        const QRect geom = window->geometry();
        if (width < 0)
            width = geom.width() - x;
        if (height < 0)
            height = geom.height() - y;
        QRect rect(geom.topLeft() + QPoint(x, y), QSize(width, height));
        rect &= window->geometry();
        return QPixmap::fromImage(m_onscreenImage).copy(rect);
    }

    return QPixmap();
}

QT_END_NAMESPACE
