// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial

#include "platform_config.h"
#include <platforminterface/rect.h>
#include <qul/eventqueue.h>
#include <platforminterface/platforminterface.h>
#include <platforminterface/drawingengine.h>
#include <platforminterface/drawingdevice.h>
#include <platforminterface/layerengine.h>
#include <platforminterface/screen.h>
#include <platforminterface/drawingdevice.h>
#include <platforminterface/error.h>

#include <QApplication>
#include <QElapsedTimer>
#include <QFile>
#include <QLocalSocket>
#include <QWidget>
#include <QMouseEvent>
#include <QPainter>
#include <QTextLayout>
#include <QTimer>
#include <QTransform>
#include <QRandomGenerator>
#include <QtCore/QBuffer>
#include <QtDebug>
#include <QWindow>
#include <QString>

#include <qul/private/imageasset.h>

#include <platform/platform.h>
#include <platform/alloc.h>
#include <platform/private/doublequeue.h>

#ifdef QUL_PLATFORM_DEVICELINK_ENABLED
#include <platform/devicelink.h>
#include <platforminterface/devicelink.h>
#endif

#include <stdlib.h>

#include <algorithm>

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

namespace {
inline QRectF toQRectF(const Qul::PlatformInterface::RectF &rect)
{
    return QRectF(rect.x(), rect.y(), rect.width(), rect.height());
}

inline QRect toQRect(const Qul::PlatformInterface::Rect &rect)
{
    return QRect(rect.x(), rect.y(), rect.width(), rect.height());
}

inline Qul::PlatformInterface::Size fromQSize(const QSize &size)
{
    return Qul::PlatformInterface::Size(size.width(), size.height());
}

inline Qul::PlatformInterface::PointF fromQPointF(const QPointF &point)
{
    return Qul::PlatformInterface::PointF(point.x(), point.y());
}

inline QPoint toQPoint(const Qul::PlatformInterface::Point &point)
{
    return QPoint(point.x(), point.y());
}

static inline QPointF toQPointF(const Qul::PlatformInterface::PointF &point)
{
    return QPointF(point.x(), point.y());
}

static inline Qul::PlatformInterface::PointF toPointF(const QPointF &point)
{
    return Qul::PlatformInterface::PointF(point.x(), point.y());
}

static inline QSize toQSize(const Qul::PlatformInterface::Size &size)
{
    return QSize(size.width(), size.height());
}

inline QTransform toQTransform(const Qul::PlatformInterface::Transform &t)
{
    return QTransform(t.m11(), t.m12(), t.m13(), t.m21(), t.m22(), t.m23(), t.m31(), t.m32(), t.m33());
}

Qul::PixelFormat toPixelFormat(QImage::Format format)
{
    switch (format) {
    case QImage::Format_ARGB32_Premultiplied:
        return Qul::PixelFormat_ARGB32_Premultiplied;
    case QImage::Format_ARGB32:
        return Qul::PixelFormat_ARGB32;
    case QImage::Format_RGB32:
        return Qul::PixelFormat_RGB32;
    case QImage::Format_RGB888:
        return Qul::PixelFormat_RGB888;
    case QImage::Format_RGB16:
        return Qul::PixelFormat_RGB16;
    case QImage::Format_ARGB4444_Premultiplied:
        return Qul::PixelFormat_ARGB4444_Premultiplied;
    case QImage::Format_Indexed8:
        return Qul::PixelFormat_RGB332;
    case QImage::Format_Alpha8:
        return Qul::PixelFormat_Alpha8;
    default:
        QUL_ASSERT(false, QulError_DrawingEngine_UnsupportedPixelFormat, format);
        return Qul::PixelFormat_ARGB32_Premultiplied;
    }
}

QImage::Format toQImageFormat(Qul::PixelFormat pixelFormat)
{
    switch (pixelFormat) {
    case Qul::PixelFormat_ARGB32_Premultiplied:
        return QImage::Format_ARGB32_Premultiplied;
    case Qul::PixelFormat_ARGB32:
        return QImage::Format_ARGB32;
    case Qul::PixelFormat_RGB32:
        return QImage::Format_RGB32;
    case Qul::PixelFormat_RGB888:
        return QImage::Format_RGB888;
    case Qul::PixelFormat_RGB16:
        return QImage::Format_RGB16;
    case Qul::PixelFormat_ARGB4444_Premultiplied:
        return QImage::Format_ARGB4444_Premultiplied;
    case Qul::PixelFormat_RGB332:
        return QImage::Format_Indexed8;
    case Qul::PixelFormat_Alpha8:
        return QImage::Format_Alpha8;
    case Qul::PixelFormat_RLE_ARGB32_Premultiplied:
        return QImage::Format_ARGB32_Premultiplied;
    case Qul::PixelFormat_RLE_RGB32:
        return QImage::Format_RGB32;
    default:
        QUL_ASSERT(false, QulError_DrawingEngine_UnsupportedPixelFormat, pixelFormat);
        return QImage::Format_ARGB32_Premultiplied;
    }
}

const auto rgb332ColorTable = [] {
    QVector<QRgb> colors(256);
    for (int c = 0; c < colors.size(); ++c) {
        uint r = (c & 0xe0) << 16;
        uint g = (c & 0x1c) << 11;
        uint b = (c & 0x3) << 6;
        b |= (b >> 2);
        colors[c] = 0xFF000000 | ((r | (r >> 3) | (r >> 6)) & 0x00FF0000) | ((g | (g >> 3) | (g >> 6)) & 0x0000FF00) | b
                    | (b >> 4);
    }
    return colors;
}();

QVector<QRgb> colorTableForPixelFormat(Qul::PixelFormat pixelFormat)
{
    switch (pixelFormat) {
    case Qul::PixelFormat_RGB332:
        return rgb332ColorTable;
    default:
        return {};
    }
}
} // namespace

namespace Qul {
namespace Platform {

static char qul_scratch_buffer[16 * 1024];

char *StackAllocator::m_buffer = qul_scratch_buffer;
char *StackAllocator::m_top = StackAllocator::m_buffer;
int32_t StackAllocator::m_size = sizeof(qul_scratch_buffer);

Qul::PlatformInterface::Size defaultApplicationSize(QUL_PLATFORM_DEFAULT_SCREEN_WIDTH,
                                                    QUL_PLATFORM_DEFAULT_SCREEN_HEIGHT);

namespace Private {

class Widget;

extern QElapsedTimer *elapsedTimer;
static QTimer *timeoutTimer = nullptr;

static char appName[] = "QUL";
static char *argv[] = {appName};
static int argc = sizeof(argv) / sizeof(*argv);

static bool visualDebug = false;
static bool chunkedFramebufferTransfer = false;
static int framebufferDataTransfers = 0;
static QString dynamicResourceBinaryString;

QPainter *painter = nullptr;

#if QUL_COLOR_DEPTH == 8
static constexpr Qul::PixelFormat screenColorFormat = Qul::PixelFormat_RGB332;
#ifdef QUL_PLATFORM_QT_USE_QPAINTER_TRANSFORMS
#error QUL_PLATFORM_QT_USE_QPAINTER_TRANSFORMS is not compatible with QUL_COLOR_DEPTH == 8
#endif
#elif QUL_COLOR_DEPTH == 16
static constexpr Qul::PixelFormat screenColorFormat = Qul::PixelFormat_RGB16;
#else
static constexpr Qul::PixelFormat screenColorFormat = Qul::PixelFormat_RGB32;
#endif

// Set partial buffer pixel count to 1/8 of a full framebuffer
static constexpr const size_t partialBufferPixelCount = QUL_PLATFORM_DEFAULT_SCREEN_WIDTH
                                                        * QUL_PLATFORM_DEFAULT_SCREEN_HEIGHT / 8;
std::shared_ptr<QImage> partialFramebuffer;
PlatformInterface::Rect partialBufferDirtyArea;

struct QtLayer;

class QtScreenData
{
public:
    void initialize(const PlatformInterface::Screen *screen);
    void presentFrame(const PlatformInterface::Rect &rect);

    void appendLayer(QtLayer *layer) { rootLayers.append(layer); }
    void removeLayer(QtLayer *layer) { rootLayers.removeOne(layer); }

    typedef QPair<QRect, quint32> DebuggingRect;

    void appendDebuggingRect(const DebuggingRect &rect) { debuggingRects.append(rect); }

    QColor backgroundColor;
    QImage::Format colorFormat;
    QVector<QRgb> colorTable;

    static QtScreenData *screenDataFor(const PlatformInterface::Screen *screen);

    const std::shared_ptr<QImage> buffer() const { return screenBuffer; }

private:
    QImage screenBufferToTargetColorFormat();
    void updateDebuggingView();

    Widget *widget = nullptr;
    Widget *debug = nullptr;

    const PlatformInterface::Screen *screen = nullptr;

    std::shared_ptr<QImage> screenBuffer;
    QList<QtLayer *> rootLayers;

    QVector<DebuggingRect> debuggingRects;
};

static QtScreenData *activeScreen = nullptr;
#ifdef QUL_PLATFORM_PARTIAL_FRAMEBUFFER
static FrameBufferingType frameBufferingType = FrameBufferingType::PartialBuffering;
#else
static FrameBufferingType frameBufferingType = FrameBufferingType::FlippedDoubleBuffering;
#endif
static int endFrameCount = 0;
static int presentedFrameCount = 0;
static bool keepHidden = false;

enum class Buffer { Back, Front };

struct TouchPointExtended : PlatformInterface::TouchPoint
{
    TouchPointExtended(int x, int y, ulong timestamp_, State state_)
        : TouchPoint(x, y, state_)
        , timestamp(timestamp_)
    {}

    TouchPointExtended()
        : TouchPoint(0, 0, TouchPoint::State::Pressed)
    {}

    ulong timestamp = 0;
};

class TouchEventQueue : public Qul::EventQueue<TouchPointExtended>
{
public:
    TouchEventQueue(const PlatformInterface::Screen *screen_)
        : screen(const_cast<PlatformInterface::Screen *>(screen_))
    {}

    void onEvent(const TouchPointExtended &touchPoint) Q_DECL_OVERRIDE
    {
        PlatformInterface::handleTouchEvent(screen, touchPoint.timestamp, &touchPoint, 1);
    }
    void onQueueOverrun() Q_DECL_OVERRIDE
    {
        clearOverrun();
        PlatformInterface::log("Touch event discarded/overwritten. Consider increasing the queue size.\n");
    }

private:
    PlatformInterface::Screen *screen;
};

uint ucs4Value(const QString &string)
{
    if (string.isEmpty()) {
        return 0;
    }
    return string.toUcs4().at(0);
}

class KeyEventQueue : public Qul::EventQueue<Qul::Private::KeyEvent>
{
public:
    KeyEventQueue() {}

    void onEvent(const Qul::Private::KeyEvent &keyEvent) Q_DECL_OVERRIDE
    {
        PlatformInterface::KeyEventType type = keyEvent.type() == Qul::Private::InputEvent::Type::KeyPress
                                                   ? PlatformInterface::KeyPressEvent
                                                   : PlatformInterface::KeyReleaseEvent;
        PlatformInterface::handleKeyEvent(keyEvent.timestamp(),
                                          type,
                                          keyEvent.key(),
                                          keyEvent.nativeScanCode(),
                                          0,
                                          NULL,
                                          false,
                                          keyEvent.ucs4());
    }
    void onQueueOverrun() Q_DECL_OVERRIDE
    {
        clearOverrun();
        PlatformInterface::log("Key event discarded/overwritten. Consider increasing the queue size.\n");
    }
};

struct QtDrawingEngine : public Qul::PlatformInterface::DrawingEngine
{
    QtDrawingEngine()
        : Qul::PlatformInterface::DrawingEngine()
    {}

    static QPainter::CompositionMode compositionMode(BlendMode blendMode)
    {
        switch (blendMode) {
        case BlendMode_Source:
            return QPainter::CompositionMode_Source;
        case BlendMode_SourceOver:
            return QPainter::CompositionMode_SourceOver;
        }
        QUL_ASSERT(false, QulError_DrawingEngine_UnsupportedBlendMode, blendMode);
        return QPainter::CompositionMode_SourceOver;
    }

    // Exclude rendering code from coverage metrics
    // LCOV_EXCL_START
    void blendAlphaMap(Qul::PlatformInterface::DrawingDevice *drawingDevice,
                       const Qul::PlatformInterface::Point &pos,
                       const Qul::PlatformInterface::Texture &source,
                       const Qul::PlatformInterface::Rect &sourceRect,
                       Qul::PlatformInterface::Rgba32 color,
                       BlendMode blendMode) override
    {
        if (source.format() < Qul::PixelFormat_Custom) {
            return drawingDevice->fallbackDrawingEngine()
                ->blendAlphaMap(drawingDevice, pos, source, sourceRect, color, blendMode);
        };
        // When PixelFormat == CUSTOM_IMAGE_FORMAT_PNG == 128 this means that image is encoded as PNG
        // and QUL did not decode the image to RAW data; rather the image data are passed as is in source.data()
        // This function implements painting png image
        // This is a reference implementation for supporting custom images
        switch (source.format()) {
        case CUSTOM_IMAGE_FORMAT_PNG: {
            if (Private::painter->isActive())
                Private::painter->drawImage(QRect{pos.x(), pos.y(), source.width(), source.height()},
                                            QImage::fromData(source.data(),
                                                             source.bytesPerLine() * source.height(),
                                                             "png"),
                                            QRect{sourceRect.x(), sourceRect.y(), source.width(), source.height()});
            break;
        }
        default:
            break;
        }
    }

    // The following functions implement blendTransformedImage / blendTransformedAlphaMap
    // using QPainter. Usually it's preferable to use the QUL fallback renderer and thereby
    // allow easy testing on desktop.
#if QUL_PLATFORM_QT_USE_QPAINTER_TRANSFORMS
    void blendTransformedImage(Qul::PlatformInterface::DrawingDevice *drawingDevice,
                               const Qul::PlatformInterface::Transform &transform,
                               const Qul::PlatformInterface::RectF &destinationRect,
                               const Qul::PlatformInterface::Texture &source,
                               const Qul::PlatformInterface::RectF &sourceRect,
                               const Qul::PlatformInterface::Rect &clipRect,
                               int sourceOpacity,
                               BlendMode blendMode) override
    {
        // ARGB4444 is not supported on desktop, must be premultiplied
        QUL_ASSERT(source.format() != Qul::PixelFormat_ARGB4444,
                   QulError_DrawingEngine_UnsupportedPixelFormat,
                   source.format());

        // scaling down by more than 50 % ?
        const bool significantDownScale = (transform.m11() < 0.5f) || (transform.m22() < 0.5f);

        QImage::Format format = toQImageFormat(source.format());

        QImage image(source.data(), source.width(), source.height(), source.bytesPerLine(), format);

        auto p = Private::painter;
        p->save();
        p->setClipRect(toQRect(clipRect));
        p->setTransform(toQTransform(transform), true);
        p->setOpacity(sourceOpacity / 256.0f);
        p->setRenderHint(QPainter::SmoothPixmapTransform);
        p->setRenderHint(QPainter::Antialiasing, significantDownScale);
        p->setCompositionMode(compositionMode(blendMode));
        p->drawImage(toQRectF(destinationRect), image, toQRectF(sourceRect));
        p->restore();
    }

    void blendTransformedAlphaMap(PlatformInterface::DrawingDevice *drawingDevice,
                                  const PlatformInterface::Transform &transform,
                                  const PlatformInterface::RectF &destinationRect,
                                  const PlatformInterface::Texture &source,
                                  const PlatformInterface::RectF &sourceRect,
                                  const PlatformInterface::Rect &clipRect,
                                  PlatformInterface::Rgba32 color,
                                  BlendMode blendMode) override
    {
        // ARGB4444 is not supported on desktop, must be premultiplied
        QUL_ASSERT(source.format() != Qul::PixelFormat_ARGB4444,
                   QulError_DrawingEngine_UnsupportedPixelFormat,
                   source.format());

        // scaling down by more than 50 % ?
        const bool significantDownScale = (transform.m11() < 0.5f) || (transform.m22() < 0.5f);

        QImage::Format format = toQImageFormat(source.format());

        QImage alphaMap(source.data(), source.width(), source.height(), source.bytesPerLine(), format);
        QImage image(source.data(), source.width(), source.height(), QImage::Format_ARGB32);
        image.fill(color.value);
        image.setAlphaChannel(alphaMap);

        auto p = Private::painter;
        p->save();
        p->setClipRect(toQRect(clipRect));
        p->setTransform(toQTransform(transform), true);
        p->setRenderHints(QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform);
        p->setRenderHint(QPainter::Antialiasing, significantDownScale);
        p->setCompositionMode(compositionMode(blendMode));
        p->drawImage(toQRectF(destinationRect), image, toQRectF(sourceRect));
        p->restore();
    }
#endif

    // LCOV_EXCL_STOP
};

static QMap<int, QtLayer *> layersById;

struct QtLayer
{
    enum Type { Sprite, Item, Image };

    QtLayer(Type type_, QtScreenData *screen_ = nullptr)
        : type(type_)
        , screen(screen_)
    {
        for (auto it = layersById.begin(); it != layersById.end(); ++it) {
            if (it.value() == this) {
                layersById.erase(it);
                break;
            }
        }
    }

    virtual int z() const = 0;
    virtual void render(QPainter *) const = 0;

    virtual void spriteLayerDestroyed() {}

    static bool compareByZ(const QtLayer *a, const QtLayer *b) { return a->z() < b->z(); }

    Type type;
    QtScreenData *screen;
};

void sortLayers(QList<QtLayer *> &layers)
{
    std::stable_sort(layers.begin(), layers.end(), QtLayer::compareByZ);
}

struct QtSpriteLayer final : public Qul::PlatformInterface::LayerEngine::SpriteLayer, public QtLayer
{
    QtSpriteLayer(QtScreenData *screen_, Qul::PlatformInterface::LayerEngine::SpriteLayerProperties props_)
        : QtLayer(Sprite, screen_)
        , props(props_)
    {
        screen->appendLayer(this);
        layersById[props.id] = this;
    }

    ~QtSpriteLayer()
    {
        for (QtLayer *layer : children)
            layer->spriteLayerDestroyed();

        screen->removeLayer(this);
    }

    Qul::PlatformInterface::LayerEngine::SpriteLayerProperties props;

    QList<QtLayer *> children;

    virtual int z() const override { return props.z; }

    virtual void render(QPainter *p) const override
    {
        if (!props.enabled)
            return;

        QImage layerImage(toQSize(props.size), QImage::Format_ARGB32_Premultiplied);
        layerImage.fill(0x0);

        QPainter layerPainter(&layerImage);

        // sprites are not supposed to be overlapping
        layerPainter.setCompositionMode(QPainter::CompositionMode_Source);

        Private::sortLayers(const_cast<QList<QtLayer *> &>(children));
        for (Private::QtLayer *layer : children)
            layer->render(&layerPainter);

        layerPainter.end();

        p->setOpacity(props.opacity);
        p->drawImage(toQPoint(props.position), layerImage);
    }
};

struct QtItemLayer final : public Qul::PlatformInterface::LayerEngine::ItemLayer, public QtLayer
{
    QtItemLayer(QtScreenData *screen_,
                Qul::PlatformInterface::LayerEngine::ItemLayerProperties props_,
                QtSpriteLayer *spriteLayer_)
        : QtLayer(Item, screen_)
        , props(props_)
        , spriteLayer(spriteLayer_)
    {
        if (spriteLayer)
            spriteLayer->children.append(this);
        else
            screen->appendLayer(this);
        layersById[props.id] = this;
    }

    ~QtItemLayer()
    {
        if (spriteLayer)
            spriteLayer->children.removeOne(this);
        else
            screen->removeLayer(this);
    }

    virtual void spriteLayerDestroyed() override { spriteLayer = nullptr; }

    std::shared_ptr<QImage> framebuffers[2] = {std::make_shared<QImage>(), std::make_shared<QImage>()};

    void updateFrameBuffers()
    {
        QImage::Format format = QImage::Format_RGB32;
        QVector<QRgb> colorTable;
        switch (props.colorDepth) {
        case Qul::PlatformInterface::LayerEngine::Bpp32Alpha:
            format = QImage::Format_ARGB32_Premultiplied;
            break;
        case Qul::PlatformInterface::LayerEngine::Bpp32:
            format = QImage::Format_RGB32;
            break;
        case Qul::PlatformInterface::LayerEngine::Bpp24:
            format = QImage::Format_RGB888;
            break;
        case Qul::PlatformInterface::LayerEngine::Bpp16Alpha:
            format = QImage::Format_ARGB4444_Premultiplied;
            break;
        case Qul::PlatformInterface::LayerEngine::Bpp16:
            format = QImage::Format_RGB16;
            break;
        case Qul::PlatformInterface::LayerEngine::Bpp8:
            format = QImage::Format_Indexed8;
            colorTable = rgb332ColorTable;
            break;
        default:
            QUL_ASSERT(false, QulError_LayerEngine_UnsupportedColorDepth, props.colorDepth);
            break;
        }

        QSize size = toQSize(props.size);
        if (framebuffers[0]->size() != size || framebuffers[0]->format() != format) {
            for (int i = 0; i < 2; ++i) {
                framebuffers[i] = std::make_shared<QImage>(size, format);
                if (format == QImage::Format_Indexed8) {
                    QUL_ASSERT(!colorTable.empty(), QulError_LayerEngine_EmptyColorTable);
                    framebuffers[i]->setColorTable(colorTable);
                }
            }
        }
    }

    int backingStoreFlipBit = 0;

    std::shared_ptr<QImage> framebufferImage(Buffer buffer)
    {
        if (frameBufferingType == FrameBufferingType::FlippedDoubleBuffering)
            return framebuffers[int(buffer) ^ backingStoreFlipBit];
        else
            return framebuffers[0];
    }

    const std::shared_ptr<QImage> framebufferImage(Buffer buffer) const
    {
        if (frameBufferingType == FrameBufferingType::FlippedDoubleBuffering)
            return framebuffers[int(buffer) ^ backingStoreFlipBit];
        else
            return framebuffers[0];
    }

    void flip()
    {
        if (frameBufferingType == FrameBufferingType::FlippedDoubleBuffering)
            backingStoreFlipBit ^= 1;
    }

    Qul::PlatformInterface::LayerEngine::ItemLayerProperties props;

    QtSpriteLayer *spriteLayer;

    virtual int z() const override { return props.z; }

    virtual void render(QPainter *p) const override
    {
        if (!props.enabled)
            return;

        p->save();
        p->translate(props.position.x(), props.position.y());
        p->setOpacity(props.opacity);
        p->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
        p->drawImage(0, 0, *framebufferImage(Buffer::Front));
        p->restore();
    }
};

struct QtImageLayer final : public Qul::PlatformInterface::LayerEngine::ImageLayer, public QtLayer
{
    QtImageLayer(QtScreenData *screen_,
                 const Qul::PlatformInterface::LayerEngine::ImageLayerProperties &props_,
                 QtSpriteLayer *spriteLayer_)
        : QtLayer(Image, screen_)
        , spriteLayer(spriteLayer_)
    {
        if (spriteLayer)
            spriteLayer->children.append(this);
        else
            screen->appendLayer(this);
        layersById[props_.id] = this;
    }

    ~QtImageLayer()
    {
        if (spriteLayer)
            spriteLayer->children.removeOne(this);
        else
            screen->removeLayer(this);
    }

    virtual void spriteLayerDestroyed() override { spriteLayer = nullptr; }

    Qul::PlatformInterface::LayerEngine::LayerPropertiesBase props;
    QImage image;
    QtSpriteLayer *spriteLayer;

    virtual int z() const override { return props.z; }

    virtual void render(QPainter *p) const override
    {
        if (!props.enabled)
            return;

        p->save();
        p->translate(props.position.x(), props.position.y());
        p->setOpacity(props.opacity);
        p->drawImage(0, 0, image);
        p->restore();
    }
};

struct QtLayerEngine : public Qul::PlatformInterface::LayerEngine
{
    ItemLayer *allocateItemLayer(const PlatformInterface::Screen *screen,
                                 const ItemLayerProperties &props,
                                 SpriteLayer *spriteLayer = nullptr) override
    {
        auto screenData = Private::QtScreenData::screenDataFor(screen);
        auto layer = new QtItemLayer(screenData, props, static_cast<QtSpriteLayer *>(spriteLayer));
        layer->updateFrameBuffers();
        return layer;
    }

    ImageLayer *allocateImageLayer(const PlatformInterface::Screen *screen,
                                   const ImageLayerProperties &props,
                                   SpriteLayer *spriteLayer = nullptr) override
    {
        auto screenData = Private::QtScreenData::screenDataFor(screen);
        auto layer = new QtImageLayer(screenData, props, static_cast<QtSpriteLayer *>(spriteLayer));
        updateImageLayer(layer, props);
        return layer;
    }

    SpriteLayer *allocateSpriteLayer(const PlatformInterface::Screen *screen,
                                     const SpriteLayerProperties &props) override
    {
        auto screenData = Private::QtScreenData::screenDataFor(screen);
        return new QtSpriteLayer(screenData, props);
    }

    void deallocateItemLayer(ItemLayer *layer) override { delete static_cast<QtItemLayer *>(layer); }
    void deallocateImageLayer(ImageLayer *layer) override { delete static_cast<QtImageLayer *>(layer); }
    void deallocateSpriteLayer(SpriteLayer *layer) override { delete static_cast<QtSpriteLayer *>(layer); }

    void updateItemLayer(ItemLayer *layer, const ItemLayerProperties &props) override
    {
        auto itemLayer = static_cast<QtItemLayer *>(layer);
        itemLayer->props = props;
        itemLayer->updateFrameBuffers();
    }

    void updateImageLayer(ImageLayer *layer, const ImageLayerProperties &props) override
    {
        auto imageLayer = static_cast<QtImageLayer *>(layer);

        imageLayer->props = props;

        const auto textureFormat = props.texture.format();
        // ARGB4444 is not supported on desktop, must be premultiplied
        QUL_ASSERT(textureFormat != Qul::PixelFormat_ARGB4444,
                   QulError_DrawingEngine_UnsupportedPixelFormat,
                   textureFormat);

        QImage::Format format;
        switch (textureFormat) {
        case PixelFormat_Alpha8:
        case PixelFormat_Custom:
        case PixelFormat_Invalid:
            imageLayer->image = QImage();
            return;
        default:
            format = toQImageFormat(textureFormat);
        }

        auto &t = props.texture;
        imageLayer->image = t.data() ? QImage(t.data(), t.width(), t.height(), t.bytesPerLine(), format) : QImage();
        if (format == QImage::Format_Indexed8)
            imageLayer->image.setColorTable(colorTableForPixelFormat(textureFormat));
    }

    void updateSpriteLayer(SpriteLayer *layer, const SpriteLayerProperties &props) override
    {
        static_cast<QtSpriteLayer *>(layer)->props = props;
    }
};

static KeyEventQueue keyEventQueue;

class Widget : public QWidget
{
public:
    Widget(const PlatformInterface::Screen *screen)
        : touchEventQueue(screen)
    {}

    // NOTE: Just be a proxy form Qt eventloop to Qul event loop, so
    // events are processed in right moment
    virtual void mouseMoveEvent(QMouseEvent *event) Q_DECL_OVERRIDE
    {
        TouchPointExtended tp{event->pos().x(),
                              event->pos().y(),
                              static_cast<ulong>(event->timestamp()),
                              TouchPointExtended::State::Moved};
        touchEventQueue.postEvent(tp);
    }

    virtual void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE
    {
        TouchPointExtended tp{event->pos().x(),
                              event->pos().y(),
                              static_cast<ulong>(event->timestamp()),
                              TouchPointExtended::State::Pressed};
        touchEventQueue.postEvent(tp);
    }

    virtual void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE
    {
        TouchPointExtended tp{event->pos().x(),
                              event->pos().y(),
                              static_cast<ulong>(event->timestamp()),
                              TouchPointExtended::State::Released};
        touchEventQueue.postEvent(tp);
    }

    virtual void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE
    {
        Qul::Private::KeyEvent ev{Qul::Private::InputEvent::Type::KeyPress,
                                  keyboardDevice,
                                  event->timestamp(),
                                  event->key(),
                                  event->nativeScanCode(),
                                  ucs4Value(event->text())};
        keyEventQueue.postEvent(ev);
    }

    virtual void keyReleaseEvent(QKeyEvent *event) Q_DECL_OVERRIDE
    {
        Qul::Private::KeyEvent ev{Qul::Private::InputEvent::Type::KeyRelease,
                                  keyboardDevice,
                                  event->timestamp(),
                                  event->key(),
                                  event->nativeScanCode(),
                                  ucs4Value(event->text())};
        keyEventQueue.postEvent(ev);
    }

    virtual void paintEvent(QPaintEvent *event) override
    {
        QUL_UNUSED(event);
        QPainter p(this);
        p.setRenderHints(QPainter::SmoothPixmapTransform);
        if (!image)
            qWarning() << "Going to paint to an image from a NULL shared pointer";

        p.drawImage(0, 0, *image);
    }

    std::shared_ptr<QImage> image = nullptr;

    TouchEventQueue touchEventQueue;
    const Qul::Private::InputDevice keyboardDevice{Qul::Private::InputDevice::Type::Keyboard, nullptr};
};

void QtScreenData::updateDebuggingView()
{
    std::shared_ptr image = std::make_shared<QImage>();
    *image = screenBuffer->copy();

    QPainter p(image.get());
    p.setOpacity(0.2);

    struct
    {
        QPainter &painter;
        void operator()(const DebuggingRect &pair) const { painter.fillRect(pair.first, QColor(pair.second)); }
    } debuggingRectPaint = {p};

    std::for_each(debuggingRects.begin(), debuggingRects.end(), debuggingRectPaint);
    p.end();
    debug->image = image;
    debug->update();

    debuggingRects.clear();
}

void QtScreenData::initialize(const PlatformInterface::Screen *s)
{
    partialFramebuffer = std::make_shared<QImage>();
    screen = s;

    QString title("QUL ");
    title.append(s->identifier());

    widget = new Widget(s);
    widget->setAttribute(Qt::WA_NoSystemBackground);
    widget->setFixedSize(toQSize(s->size()));
    widget->setWindowTitle(title);

    if (visualDebug) {
        debug = new Widget(s);
        debug->setAttribute(Qt::WA_NoSystemBackground);
        debug->resize(toQSize(s->size()));
        debug->show();
    }

    // Screen buffer will always be 32bpp, frame buffer color depth
    // is controlled by the layers system. QPainter is not able
    // to draw on all buffer formats (e.g. Indexed8) so we are not even
    // able to implement matching (QUL_COLOR_DEPTH) screen buffer format RGB332.
    // Screen buffer is converted to the desired format in presentFrame() after
    // QPainter did it's job on the ARGB32 buffer.
    // screenBuffer format doesn't really matter for testing the software renderer,
    // as all Qul specific color conversions (e.g. from blendfunctions.h)
    // are made when drawing to to layers frame buffer.
    screenBuffer = std::make_shared<QImage>(toQSize(s->size()), QImage::Format_ARGB32);
    backgroundColor = QColor(s->backgroundColor().value);
    colorFormat = toQImageFormat(s->colorFormat());
    colorTable = colorTableForPixelFormat(s->colorFormat());
}

void QtScreenData::presentFrame(const PlatformInterface::Rect &)
{
    QPainter painterInstance(screenBuffer.get());
    painterInstance.setCompositionMode(QPainter::CompositionMode_Source);
    painterInstance.fillRect(screenBuffer->rect(), backgroundColor);
    painterInstance.setCompositionMode(QPainter::CompositionMode_SourceOver);

    sortLayers(rootLayers);
    for (QtLayer *layer : rootLayers)
        layer->render(&painterInstance);

    painterInstance.end();

    widget->image = std::make_shared<QImage>(screenBufferToTargetColorFormat());
    widget->update();

    if (!widget->isVisible() && !keepHidden) {
        // We have to wait for the widget to be shown until after the first presentFrame call.
        // Otherwise the widget->image has not been set and undetermined content will show.
        widget->show();
    }

    if (visualDebug)
        updateDebuggingView();
}

QImage QtScreenData::screenBufferToTargetColorFormat()
{
    if (screen->colorFormat() == PixelFormat_RGB332) {
        // Default Qt color conversion to Format_Indexed8 is so painfully slow...
        // Make things faster, as we don't need to lookup color table.
        QUL_ASSERT(screenBuffer->format() == QImage::Format_ARGB32,
                   QulError_DrawingEngine_UnsupportedPixelFormat,
                   screenBuffer->format());
        QImage dst(screenBuffer->width(), screenBuffer->height(), QImage::Format_Indexed8);
        for (int y = 0; y < screenBuffer->height(); ++y) {
            auto srcLine = reinterpret_cast<uint *>(screenBuffer->scanLine(y));
            auto dstLine = dst.scanLine(y);
            for (int x = 0; x < screenBuffer->width(); ++x) {
                const auto &c = srcLine[x];
                dstLine[x] = (((c) >> 6) & 0x3) | (((c) >> 11) & 0x1c) | (((c) >> 16) & 0xe0);
            }
        }
        dst.setColorTable(colorTable);
        return dst;
    }

    return screenBuffer->convertToFormat(colorFormat, colorTable, Qt::AvoidDither);
}

QtScreenData *QtScreenData::screenDataFor(const PlatformInterface::Screen *screen)
{
    size_t count;
    Qul::PlatformInterface::Screen *screens = getPlatformInstance()->availableScreens(&count);

    static QtScreenData *screenData = nullptr;
    if (!screenData)
        screenData = new QtScreenData[count];

    int index = screen - screens;
    QUL_ASSERT(index >= 0 && index < static_cast<int>(count), QulError_DrawingEngine_InvalidScreen);
    return &screenData[index];
}

class ScheduleUpdateEvent : public QEvent
{
public:
    static constexpr QEvent::Type Type = static_cast<QEvent::Type>(QEvent::User + 1);

    ScheduleUpdateEvent(uint64_t timeout)
        : QEvent(Type)
        , m_timeout(timeout)
    {}

    uint64_t timeout() const { return m_timeout; }

private:
    uint64_t m_timeout = 0;
};

class UpdateSchdeuler : public QObject
{
public:
    bool event(QEvent *event) override
    {
        if (event->type() != ScheduleUpdateEvent::Type) {
            return QObject::event(event);
        }

        if (Private::timeoutTimer)
            Private::timeoutTimer->stop();

        quint64 currentTime = getPlatformInstance()->currentTimestamp();

        // Throttle at ~60 fps here, since the rendering is non-blocking
        static const auto refreshInterval = 16u;
        const auto nextVsync = lastTimeout + refreshInterval;
        auto scheduleEvent = static_cast<ScheduleUpdateEvent *>(event);
        nextTimeout = qMax(scheduleEvent->timeout(), nextVsync);

        quint64 delta = 0;
        if (nextTimeout > currentTime)
            delta = nextTimeout - currentTime;

        // QTimer::start() takes int
        delta = qMin(delta, quint64(INT_MAX));

        if (Private::timeoutTimer)
            Private::timeoutTimer->start(delta);

        return true;
    }

    uint64_t lastTimeout = 0;
    uint64_t nextTimeout = 0;
};
UpdateSchdeuler scheduler;
QElapsedTimer *elapsedTimer = nullptr;

} // namespace Private

uint64_t test_nextScheduledUpdate()
{
    getPlatformInstance()->update(); // Make sure ScheduleUpdateEvent event was processed
    return Private::scheduler.nextTimeout;
}

QTimer *test_timeoutTimer()
{
    return Private::timeoutTimer;
}

void timeout()
{
    Private::scheduler.lastTimeout = getPlatformInstance()->currentTimestamp();
    Qul::PlatformInterface::updateEngine(Private::scheduler.lastTimeout);
}

FrameBufferingType frameBufferingType()
{
    return Private::frameBufferingType;
}

std::pair<void *, void *> stackRange()
{
    return std::make_pair(nullptr, nullptr);
}

QMap<PlatformInterface::MemoryAllocator::AllocationType, int> allocationCounts;

class QtMemoryAllocator : public PlatformInterface::MemoryAllocator
{
public:
    QtMemoryAllocator(PlatformInterface::MemoryAllocator::AllocationType type)
        : m_type(type)
    {}

    void *allocate(std::size_t size, UsagePattern usagePattern)
    {
        ++allocationCounts[m_type];
        return MemoryAllocator::allocate(size, usagePattern);
    }

private:
    PlatformInterface::MemoryAllocator::AllocationType m_type;
};

QMap<PlatformInterface::MemoryAllocator::AllocationType, QSharedPointer<QtMemoryAllocator> > memoryAllocators;

QtMemoryAllocator *qtAllocator(PlatformInterface::MemoryAllocator::AllocationType type)
{
    if (!memoryAllocators.contains(type))
        memoryAllocators[type] = QSharedPointer<QtMemoryAllocator>(new QtMemoryAllocator(type));

    return memoryAllocators[type].get();
}

static std::optional<int> numberOfFramesToPreserveAssetsOverride;

struct QtPlatform : PlatformContext
{
    static void qulErrorHandler(enum QulError code, unsigned int lineNumber, int param1, int param2, int param3)
    {
        QString errorMessage
            = QString("Qt for MCUs error: %1:%2:%3, line: %4, params: %5, %6, %7.\r\nSee "
                      "https://doc.qt.io/QtForMCUs/qul-errorcode-h.html#QulError-enum for more information.\r\n")
                  .arg(Qul::errorCodeToString(code < QulError_Core_End       ? QulError_Core
                                              : code < QulError_Platform_End ? QulError_Platform
                                                                             : QulError_Custom))
                  .arg(Qul::errorCodeToString(code))
                  .arg((int) code)
                  .arg(lineNumber)
                  .arg(param1)
                  .arg(param2)
                  .arg(param3);

        PlatformInterface::log("%s", errorMessage.toLatin1().constData());
        exit(EXIT_FAILURE);
    }

    Config config() const override
    {
        Config cfg = PlatformContext::config();
        if (numberOfFramesToPreserveAssetsOverride)
            cfg.numberOfFramesToPreserveAssets = *numberOfFramesToPreserveAssetsOverride;
        return cfg;
    }

    void initializePlatform() override
    {
        // Desktop adaptation requires a custom error handler to stop execution when an error is detected
        QulErrorHandler *prevHandler = Qul::setErrorHandler(qulErrorHandler);
        if (prevHandler != NULL) {
            // Keep the error handler set by the user
            Qul::setErrorHandler(prevHandler);
        }
        Qul::PlatformInterface::initializeRgb332CpuFallbackDrawingEngine();
        Qul::PlatformInterface::initializeRgb16CpuFallbackDrawingEngine();
        Qul::PlatformInterface::initializeArgb4444CpuFallbackDrawingEngine();
        Qul::PlatformInterface::initializeRgb888CpuFallbackDrawingEngine();
        Qul::PlatformInterface::initializeArgb32CpuFallbackDrawingEngine();

        setbuf(stdout, nullptr);
        setbuf(stderr, nullptr);

        QUL_IS_ENV_SET("QUL_VISUAL_DEBUG", Private::visualDebug);

#ifdef QUL_CC_MSVC
        for (int i = 0; i < Qul::Private::qrcNumAssetDataSegments; ++i) {
            auto storageSectionName = Qul::Private::qrcAssetDataSegmentNames[i];
            HRSRC resourceInfo = FindResourceA(nullptr, storageSectionName, "BINARY");
            if (!resourceInfo) {
                qWarning() << "FindResourceA: Resource for storage section " << storageSectionName << " not found";
                QUL_ASSERT(false, QulError_ResourceStorageSection_LoadFailed);
                continue;
            }
            HGLOBAL resource = LoadResource(nullptr, resourceInfo);
            if (!resource) {
                qWarning() << "LoadResource: Unable to load resource for storage section " << storageSectionName;
                QUL_ASSERT(false, QulError_ResourceStorageSection_LoadFailed);
                continue;
            }
            Qul::Private::qrcAssetDataSegments[i].address = resource;
        }
#endif

        QString dynamicResourceDataBinaryOption;
        QUL_GETENV_OR_DEFAULT("QUL_RESOURCE_BINARIES",
                              dynamicResourceDataBinaryOption,
                              Qul::Platform::Private::dynamicResourceBinaryString);

        QMap<QString, QString> dynamicResourceDataBinaries;

        QStringList list = dynamicResourceDataBinaryOption.split(";");
        for (int i = 0; i < list.size() / 2; ++i) {
            dynamicResourceDataBinaries[list[2 * i]] = list[2 * i + 1];
        }

        for (int i = 0; i < Qul::Private::qrcNumAssetDataSegments; ++i) {
            auto storageSectionName = Qul::Private::qrcAssetDataSegmentNames[i];
            if (dynamicResourceDataBinaries.contains(storageSectionName)) {
                auto filePath = dynamicResourceDataBinaries[storageSectionName];
                QFile file(filePath);
                if (file.open(QIODevice::ReadOnly)) {
                    QByteArray *data = new QByteArray(file.readAll());
                    qWarning("Overriding resource storage section %s with data from file: %s",
                             qPrintable(storageSectionName),
                             qPrintable(filePath));
                    Qul::Private::qrcAssetDataSegments[i].address = data->data();
                } else {
                    qWarning("Failed to open file %s: %s", qPrintable(filePath), qPrintable(file.errorString()));
                }
                dynamicResourceDataBinaries.remove(storageSectionName);
            }
        }

        for (auto storageSectionName : dynamicResourceDataBinaries.keys()) {
            qWarning() << "Resource storage section" << storageSectionName
                       << "is not a part of the application, ignored";
        }
    }

    void initializeDisplay(const PlatformInterface::Screen *s) override
    {
        // QCoreApplication already exists when run under test
        if (!QCoreApplication::instance()) {
            (void) new QApplication(Private::argc, Private::argv);

            Private::elapsedTimer = new QElapsedTimer;
            Private::elapsedTimer->start();

#ifdef QUL_PLATFORM_DEVICELINK_ENABLED
            if (Qul::Platform::DeviceLink::instance()) {
                Qul::Platform::DeviceLink::instance()->init();
            }
#endif
        }

        // timeoutTimer should always be created - even for tests
        if (!Private::timeoutTimer) {
            Private::timeoutTimer = new QTimer;
            Private::timeoutTimer->setSingleShot(true);

            QObject::connect(Private::timeoutTimer, &QTimer::timeout, timeout);
        }

        auto screenData = Private::QtScreenData::screenDataFor(s);
        screenData->initialize(s);
    }

    uint64_t update() override
    {
        qApp->processEvents(QEventLoop::AllEvents, 16);
        return currentTimestamp() + 16;
    }

    void exec() override
    {
        qApp->exec();
        delete Private::timeoutTimer;
        Private::timeoutTimer = nullptr;
    }

    Qul::PlatformInterface::Screen *availableScreens(size_t *screenCount) const override
    {
        static PlatformInterface::Screen *screens = nullptr;

        constexpr int NumScreens = 2;

        if (!screens) {
            screens = new PlatformInterface::Screen[NumScreens]{
                PlatformInterface::Screen(PlatformInterface::Size(QUL_PLATFORM_DEFAULT_SCREEN_WIDTH,
                                                                  QUL_PLATFORM_DEFAULT_SCREEN_HEIGHT),
                                          Private::screenColorFormat,
                                          "Primary screen",
                                          true,
                                          defaultApplicationSize),
                PlatformInterface::Screen(PlatformInterface::Size(QUL_PLATFORM_DEFAULT_SCREEN_WIDTH,
                                                                  QUL_PLATFORM_DEFAULT_SCREEN_HEIGHT),
                                          Private::screenColorFormat,
                                          "Secondary screen",
                                          true,
                                          defaultApplicationSize)};
        }

        *screenCount = NumScreens;
        return screens;
    }

    FrameBufferingType frameBufferingType(const PlatformInterface::LayerEngine::ItemLayer *layer) const override
    {
        Q_UNUSED(layer);

        return Private::frameBufferingType;
    }

    PlatformInterface::LayerEngine *layerEngine() override
    {
        static Private::QtLayerEngine layerEngine;
        return &layerEngine;
    }

    PlatformInterface::DrawingDevice *beginFrame(const PlatformInterface::LayerEngine::ItemLayer *layer,
                                                 const PlatformInterface::Rect &rect,
                                                 int refreshInterval) override
    {
        Q_UNUSED(refreshInterval);

        if (!Private::painter)
            Private::painter = new QPainter;

        Private::QtItemLayer *itemLayer = const_cast<Private::QtItemLayer *>(
            static_cast<const Private::QtItemLayer *>(layer));

        Private::activeScreen = itemLayer->screen;

        std::shared_ptr<QImage> image = itemLayer->framebufferImage(Qul::Platform::Private::Buffer::Back);
        std::shared_ptr<QImage> drawBuffer;

        Qul::PlatformInterface::Size deviceSize;
        PlatformInterface::Rect clipRect = rect;

        if (frameBufferingType(layer) == FrameBufferingType::PartialBuffering) {
            Private::partialBufferDirtyArea = rect;
            clipRect.moveTo(0, 0);
            deviceSize = Qul::PlatformInterface::Size(rect.width(), rect.height());
            Private::partialFramebuffer = std::make_shared<QImage>(toQSize(deviceSize), image->format());
            drawBuffer = Private::partialFramebuffer;
        } else {
            deviceSize = itemLayer->props.size;
            drawBuffer = image;
        }

        // QPainter does not allow drawing on Format_Indexed8 buffers
        if (drawBuffer->format() != QImage::Format_Indexed8) {
            Private::painter->begin(drawBuffer.get());
            Private::painter->setClipRect(toQRect(clipRect));
        }

        static Private::QtDrawingEngine drawingEngine;
        this->buffer = new PlatformInterface::DrawingDevice(toPixelFormat(drawBuffer->format()),
                                                            deviceSize,
                                                            drawBuffer->bits(),
                                                            drawBuffer->bytesPerLine(),
                                                            &drawingEngine,
                                                            Private::painter);

        return this->buffer;
    }

    void endFrame(const PlatformInterface::LayerEngine::ItemLayer *layer) override
    {
        ++Private::endFrameCount;

        if (Private::painter->isActive())
            Private::painter->end();

        if (frameBufferingType(layer) == FrameBufferingType::PartialBuffering) {
            Private::QtItemLayer *itemLayer = const_cast<Private::QtItemLayer *>(
                static_cast<const Private::QtItemLayer *>(layer));

            auto framebuffer = itemLayer->framebufferImage(Qul::Platform::Private::Buffer::Back);
            auto partialFramebuffer = Private::partialFramebuffer;

            auto framebufferGrayscale8 = std::make_shared<QImage>(framebuffer->bits(),
                                                                  framebuffer->width(),
                                                                  framebuffer->height(),
                                                                  framebuffer->bytesPerLine(),
                                                                  QImage::Format_Grayscale8);
            auto partialFramebufferGrayscale8 = std::make_shared<QImage>(partialFramebuffer->bits(),
                                                                         partialFramebuffer->width(),
                                                                         partialFramebuffer->height(),
                                                                         partialFramebuffer->bytesPerLine(),
                                                                         QImage::Format_Grayscale8);

            if (framebuffer->format() == QImage::Format_Indexed8) {
                // QPainter does not allow drawing on Format_Indexed8 buffers
                framebuffer = framebufferGrayscale8;
                partialFramebuffer = partialFramebufferGrayscale8;
            }

            QPainter painterInstance(framebuffer.get());
            painterInstance.setCompositionMode(QPainter::CompositionMode_Source);
            painterInstance.drawImage(Private::partialBufferDirtyArea.x(),
                                      Private::partialBufferDirtyArea.y(),
                                      *partialFramebuffer);
            painterInstance.end();
        }

        delete this->buffer;
        this->buffer = nullptr;

        Private::QtItemLayer *itemLayer = const_cast<Private::QtItemLayer *>(
            static_cast<const Private::QtItemLayer *>(layer));
        itemLayer->flip();
    }

    FrameStatistics presentFrame(const PlatformInterface::Screen *screen, const PlatformInterface::Rect &rect) override
    {
        ++Private::presentedFrameCount;
        auto screenData = Private::QtScreenData::screenDataFor(screen);
        screenData->backgroundColor = QColor(screen->backgroundColor().value);
        screenData->presentFrame(rect);
        return FrameStatistics();
    }

    void scheduleEngineUpdate(uint64_t timeout) override
    {
        auto updateEvent = new (std::nothrow) Private::ScheduleUpdateEvent{timeout};
        if (QUL_UNLIKELY(!updateEvent)) {
            std::puts("Error: Allocation failure while scheduling engine update. Application might misbehave.");
            return;
        }
        qApp->postEvent(&Private::scheduler, updateEvent);
    }

    double rand() override { return QRandomGenerator::global()->generateDouble(); }

    uint64_t currentTimestamp() override { return Private::elapsedTimer ? Private::elapsedTimer->elapsed() : 0; }

    PlatformInterface::MemoryAllocator *memoryAllocator(PlatformInterface::MemoryAllocator::AllocationType type) override
    {
        return qtAllocator(type);
    }

    void consoleWrite(char character) override { std::putchar(character); }

    size_t partialBufferPixelCount(const PlatformInterface::LayerEngine::ItemLayer *layer) const override
    {
        Q_UNUSED(layer);
        return Private::partialBufferPixelCount;
    }

private:
    PlatformInterface::DrawingDevice *buffer = nullptr;
};

PlatformContext *getPlatformInstance()
{
    static QtPlatform platform;
    return &platform;
}

} // namespace Platform

namespace Private {

void setDefaultApplicationSize(int width, int height)
{
    Qul::Platform::defaultApplicationSize = Qul::PlatformInterface::Size(width, height);
}

void emitDebuggingRect(const PlatformInterface::Rect &rect, quint32 col)
{
    if (Platform::Private::activeScreen)
        Platform::Private::activeScreen->appendDebuggingRect({toQRect(rect), col});
}

const uchar *framebufferDataForScreen(Qul::PlatformInterface::Screen *screen,
                                      Qul::PixelFormat *pixelFormat,
                                      uint32_t *width,
                                      uint32_t *height,
                                      uint32_t *bytesPerLine)
{
    auto screenData = Platform::Private::QtScreenData::screenDataFor(screen);
    const std::shared_ptr<QImage> image = screenData->buffer();

    *pixelFormat = toPixelFormat(image->format());
    *width = image->width();
    *height = image->height();
    *bytesPerLine = image->bytesPerLine();

    return image->bits();
}

const uchar *framebufferDataForLayer(
    int platformId, Qul::PixelFormat *pixelFormat, uint32_t *width, uint32_t *height, uint32_t *bytesPerLine)
{
    auto it = Platform::Private::layersById.find(platformId);
    if (it == Platform::Private::layersById.end())
        return nullptr;

    auto layer = *it;
    if (layer->type == Platform::Private::QtLayer::Item) {
        auto itemLayer = static_cast<Platform::Private::QtItemLayer *>(layer);
        const std::shared_ptr<QImage> image = itemLayer->framebufferImage(Platform::Private::Buffer::Front);

        *pixelFormat = toPixelFormat(image->format());
        *width = image->width();
        *height = image->height();
        *bytesPerLine = image->bytesPerLine();

        return image->bits();
    }

    return nullptr;
}

void setNumberOfFramesToPreserveAssets(int numberOfFramesToPreserveAssets)
{
    Platform::numberOfFramesToPreserveAssetsOverride = numberOfFramesToPreserveAssets;
}

void setFrameBufferingType(Qul::Platform::FrameBufferingType type)
{
    Platform::Private::frameBufferingType = type;
}

void setKeepHidden(bool keepHidden)
{
    Platform::Private::keepHidden = keepHidden;
}

int getPresentedFrameCount()
{
    return Platform::Private::presentedFrameCount;
}

int getEndFrameCount()
{
    return Platform::Private::endFrameCount;
}

void setChunkedFramebufferTransfer(bool chunkedFramebufferTransfer)
{
    Platform::Private::chunkedFramebufferTransfer = chunkedFramebufferTransfer;
}

int getFramebufferDataTransferCount()
{
    return Platform::Private::framebufferDataTransfers;
}

int allocationCount(PlatformInterface::MemoryAllocator::AllocationType type)
{
    return Platform::allocationCounts[type];
}

void setDynamicResourceBinaries(const char *dynamicResourceBinaryString)
{
    Platform::Private::dynamicResourceBinaryString = QString(dynamicResourceBinaryString);
}

} // namespace Private

namespace Platform {

#ifdef QUL_PLATFORM_DEVICELINK_ENABLED
class QtDeviceLinkInterface : public QObject, public Qul::Platform::DeviceLinkInterface
{
public:
    QtDeviceLinkInterface()
        : socket(nullptr)
    {}

    void platformInit() override
    {
        const QString socketAddress = qEnvironmentVariable("DEVICE_LINK_SOCKET");
        if (socketAddress.isEmpty()) {
            fprintf(stderr, "Device Link connection failed: DEVICE_LINK_SOCKET environment variable is not set.\n");
            return;
        }
        socket = new QLocalSocket(this);
        connect(socket, &QLocalSocket::connected, this, &QtDeviceLinkInterface::onSocketConnected);
        connect(socket, &QLocalSocket::disconnected, this, &QtDeviceLinkInterface::onSocketDisconnected);
        connect(socket, &QLocalSocket::errorOccurred, this, &QtDeviceLinkInterface::onSocketErrorOccurred);
        connect(socket, &QLocalSocket::readyRead, this, &QtDeviceLinkInterface::onReadyRead);
        socket->connectToServer(socketAddress);
        if (!socket) {
            return;
        }
        if (!socket->waitForConnected()) {
            reportSocketError("Connection timed out");
        }
    }

    void transmitChars(const uint8_t *data, uint32_t size) override
    {
        if (socket) {
            while (size > 0) {
                qint64 written = socket->write(reinterpret_cast<const char *>(data), size);
                if (written < 0) {
                    reportSocketError(socket->errorString());
                    return;
                }
                data += written;
                size -= written;
            }
        } else {
            // TODO: should we prefer OutputDebugString on Windows?
            if (fwrite(data, 1, size, stdout) < size || fflush(stdout) != 0) {
                fprintf(stderr, "Error: cannot write the log to the standard output stream\n");
            }
        }
    }

    std::shared_ptr<QImage> getImage(const PlatformInterface::LayerEngine::ItemLayer *layer)
    {
        using namespace Qul::Platform::Private;
        if (!layer) {
            size_t screenCount;
            PlatformInterface::Screen *screen = getPlatformInstance()->availableScreens(&screenCount);
            return QtScreenData::screenDataFor(screen)->buffer();
        } else {
            return static_cast<const QtItemLayer *>(layer)->framebufferImage(Buffer::Front);
        }
    }

    ChunkedFramebufferDataTransferSizes chunkedFramebufferDataTransferSizes(
        const PlatformInterface::LayerEngine::ItemLayer *) const
    {
        return ChunkedFramebufferDataTransferSizes{20, 10};
    }

    FramebufferDataChunk requestFramebufferDataChunk(const Qul::PlatformInterface::LayerEngine::ItemLayer *layer,
                                                     const PlatformInterface::Rect &rect) override
    {
        using namespace Qul::Platform::Private;
        std::shared_ptr<QImage> image = getImage(layer);

        FramebufferDataChunk chunk;
        chunk.address = static_cast<uchar *>(image->scanLine(rect.y())) + (rect.x() * image->depth() / 8);
        chunk.bytesPerLine = image->bytesPerLine();
        return chunk;
    }

    void framebufferDataTransferComplete() override { ++Private::framebufferDataTransfers; }

    FramebufferFormat framebufferFormat(const PlatformInterface::LayerEngine::ItemLayer *layer) override
    {
        using namespace Qul::Platform::Private;
        std::shared_ptr<QImage> image = getImage(layer);

        FramebufferFormat format;
        format.address = Private::chunkedFramebufferTransfer ? nullptr : static_cast<uchar *>(image->bits());
        format.width = image->width();
        format.height = image->height();
        format.bitsPerPixel = image->depth();
        format.bytesPerLine = image->bytesPerLine();

        switch (image->format()) {
        case QImage::Format_ARGB32_Premultiplied:
        case QImage::Format_RGB32:
        case QImage::Format_RGB888:
            format.redChannel = {16, 8};
            format.greenChannel = {8, 8};
            format.blueChannel = {0, 8};
            format.swapBytes = 0;
            break;
        case QImage::Format_ARGB4444_Premultiplied:
            format.redChannel = {8, 4};
            format.greenChannel = {4, 4};
            format.blueChannel = {0, 4};
            format.swapBytes = 2;
            break;
        case QImage::Format_RGB16:
            format.redChannel = {0, 5};
            format.greenChannel = {5, 6};
            format.blueChannel = {11, 5};
            format.swapBytes = 2;
            break;
        case QImage::Format_Indexed8:
            format.redChannel = {6, 2};
            format.greenChannel = {3, 3};
            format.blueChannel = {0, 3};
            format.swapBytes = 0;
            break;
        default:
            break;
        }
        return format;
    }

private slots:
    void onSocketConnected() {}

    void onSocketDisconnected() { socket = nullptr; }

    void onSocketErrorOccurred(QLocalSocket::LocalSocketError) { reportSocketError(socket->errorString()); }

    void onReadyRead()
    {
        const QByteArray data = socket->readAll();
        Qul::PlatformInterface::deviceLinkBytesReceived(reinterpret_cast<const uint8_t *>(data.data()), data.size());
    }

private:
    QLocalSocket *socket;

    void reportSocketError(const QString &message)
    {
        fprintf(stderr, "Device Link socket error: %s\n", qPrintable(message));
        socket->deleteLater();
        socket = nullptr;
    }
};

DeviceLinkInterface *getDeviceLinkInterface()
{
    static QtDeviceLinkInterface deviceLinkInterface;
    return &deviceLinkInterface;
}
#endif

} // namespace Platform

} // namespace Qul
