/*
    Copyright 2016-2024 melonDS team

    This file is part of melonDS.

    melonDS 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 3 of the License, or (at your option)
    any later version.

    melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/.
*/

#include <QEventLoop>

#include "CameraManager.h"
#include "Config.h"

using namespace melonDS;

const char* kCamConfigPath[] = {"DSi.Camera0", "DSi.Camera1"};

#if QT_VERSION >= 0x060000

CameraFrameDumper::CameraFrameDumper(QObject* parent) : QVideoSink(parent)
{
    cam = (CameraManager*)parent;

    connect(this, &CameraFrameDumper::videoFrameChanged, this, &CameraFrameDumper::present);
}

void CameraFrameDumper::present(const QVideoFrame& _frame)
{
    QVideoFrame frame(_frame);
    if (!frame.map(QVideoFrame::ReadOnly))
        return;
    if (!frame.isReadable())
    {
        frame.unmap();
        return;
    }

    switch (frame.pixelFormat())
    {
    case QVideoFrameFormat::Format_XRGB8888:
    case QVideoFrameFormat::Format_YUYV:
        cam->feedFrame((u32*)frame.bits(0), frame.width(), frame.height(), frame.pixelFormat() == QVideoFrameFormat::Format_YUYV);
        break;

    case QVideoFrameFormat::Format_UYVY:
        cam->feedFrame_UYVY((u32*)frame.bits(0), frame.width(), frame.height());
        break;

    case QVideoFrameFormat::Format_NV12:
        cam->feedFrame_NV12((u8*)frame.bits(0), (u8*)frame.bits(1), frame.width(), frame.height());
        break;
    }

    frame.unmap();
}

#else

CameraFrameDumper::CameraFrameDumper(QObject* parent) : QAbstractVideoSurface(parent)
{
    cam = (CameraManager*)parent;
}

bool CameraFrameDumper::present(const QVideoFrame& _frame)
{
    QVideoFrame frame(_frame);
    if (!frame.map(QAbstractVideoBuffer::ReadOnly))
        return false;
    if (!frame.isReadable())
    {
        frame.unmap();
        return false;
    }

    switch (frame.pixelFormat())
    {
    case QVideoFrame::Format_RGB32:
    case QVideoFrame::Format_YUYV:
        cam->feedFrame((u32*)frame.bits(0), frame.width(), frame.height(), frame.pixelFormat() == QVideoFrame::Format_YUYV);
        break;

    case QVideoFrame::Format_UYVY:
        cam->feedFrame_UYVY((u32*)frame.bits(0), frame.width(), frame.height());
        break;

    case QVideoFrame::Format_NV12:
        cam->feedFrame_NV12((u8*)frame.bits(0), (u8*)frame.bits(1), frame.width(), frame.height());
        break;
    }

    frame.unmap();

    return true;
}

QList<QVideoFrame::PixelFormat> CameraFrameDumper::supportedPixelFormats(QAbstractVideoBuffer::HandleType type) const
{
    QList<QVideoFrame::PixelFormat> ret;

    ret.append(QVideoFrame::Format_RGB32);
    ret.append(QVideoFrame::Format_YUYV);
    ret.append(QVideoFrame::Format_UYVY);
    ret.append(QVideoFrame::Format_NV12);

    return ret;
}

#endif


CameraManager::CameraManager(int num, int width, int height, bool yuv)
    : QObject(),
    num(num),
    config(Config::GetGlobalTable().GetTable(kCamConfigPath[num]))
{
    startNum = 0;

    // QCamera needs to be controlled from the UI thread, hence this
    connect(this, SIGNAL(camStartSignal()), this, SLOT(camStart()));
    connect(this, SIGNAL(camStopSignal()), this, SLOT(camStop()));

    frameWidth = width;
    frameHeight = height;
    frameFormatYUV = yuv;

    int fbsize = frameWidth * frameHeight;
    if (yuv) fbsize /= 2;
    frameBuffer = new u32[fbsize];
    tempFrameBuffer = new u32[fbsize];

    inputType = -1;
    xFlip = config.GetBool("XFlip");
    init();
}

CameraManager::~CameraManager()
{
    deInit();

    // save settings here?

    delete[] frameBuffer;
    delete[] tempFrameBuffer;
}

void CameraManager::init()
{
    if (inputType != -1)
        deInit();

    startNum = 0;

    inputType = config.GetInt("InputType");
    imagePath = config.GetQString("ImagePath");
    camDeviceName = config.GetQString("DeviceName");

    camDevice = nullptr;

    {
        // fill the framebuffer with black

        int total = frameWidth * frameHeight;
        u32 fill = 0;
        if (frameFormatYUV)
        {
            total /= 2;
            fill = 0x80008000;
        }

        for (int i = 0; i < total; i++)
            frameBuffer[i] = fill;
    }

    if (inputType == 1)
    {
        // still image

        QImage img(imagePath);
        if (!img.isNull())
        {
            QImage imgconv = img.convertToFormat(QImage::Format_RGB32);
            if (frameFormatYUV)
            {
                copyFrame_RGBtoYUV((u32*)img.bits(), img.width(), img.height(),
                                   frameBuffer, frameWidth, frameHeight,
                                   false);
            }
            else
            {
                copyFrame_Straight((u32*)img.bits(), img.width(), img.height(),
                                   frameBuffer, frameWidth, frameHeight,
                                   false, false);
            }
        }
    }
    else if (inputType == 2)
    {
        // physical camera

#if QT_VERSION >= 0x060000
        const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();
        for (const QCameraDevice& cam : cameras)
        {
            if (QString(cam.id()) == camDeviceName)
            {
                camDevice = new QCamera(cam);
                break;
            }
        }

        if (camDevice)
        {
            const QList<QCameraFormat> supported = camDevice->cameraDevice().videoFormats();
            bool good = false;
            for (const QCameraFormat& item : supported)
            {
                if (item.pixelFormat() != QVideoFrameFormat::Format_YUYV &&
                    item.pixelFormat() != QVideoFrameFormat::Format_UYVY &&
                    item.pixelFormat() != QVideoFrameFormat::Format_NV12 &&
                    item.pixelFormat() != QVideoFrameFormat::Format_XRGB8888)
                    continue;

                if (item.resolution().width() != 640 && item.resolution().height() != 480)
                    continue;

                camDevice->setCameraFormat(item);
                good = true;
                break;
            }

            if (!good)
            {
                delete camDevice;
                camDevice = nullptr;
            }
            else
            {
                camDumper = new CameraFrameDumper(this);

                camSession = new QMediaCaptureSession(this);
                camSession->setCamera(camDevice);
                camSession->setVideoOutput(camDumper);
            }
        }
#else
        camDevice = new QCamera(camDeviceName.toUtf8());
        if (camDevice->error() != QCamera::NoError)
        {
            delete camDevice;
            camDevice = nullptr;
        }

        if (camDevice)
        {
            camDevice->load();
            if (camDevice->status() == QCamera::LoadingStatus)
            {
                QEventLoop loop;
                connect(camDevice, &QCamera::statusChanged, &loop, &QEventLoop::quit);
                loop.exec();
            }

            const QList<QCameraViewfinderSettings> supported = camDevice->supportedViewfinderSettings();
            bool good = false;
            for (const QCameraViewfinderSettings& item : supported)
            {
                if (item.pixelFormat() != QVideoFrame::Format_YUYV &&
                    item.pixelFormat() != QVideoFrame::Format_UYVY &&
                    item.pixelFormat() != QVideoFrame::Format_NV12 &&
                    item.pixelFormat() != QVideoFrame::Format_RGB32)
                    continue;

                if (item.resolution().width() != 640 && item.resolution().height() != 480)
                    continue;

                camDevice->setViewfinderSettings(item);
                good = true;
                break;
            }

            camDevice->unload();

            if (!good)
            {
                delete camDevice;
                camDevice = nullptr;
            }
            else
            {
                camDumper = new CameraFrameDumper(this);
                camDevice->setViewfinder(camDumper);
            }
        }
#endif
    }
}

void CameraManager::deInit()
{
    if (inputType == 2)
    {
        if (camDevice)
        {
            camDevice->stop();
            delete camDevice;
            delete camDumper;
#if QT_VERSION >= 0x060000
            delete camSession;
#endif
        }
    }

    camDevice = nullptr;
    inputType = -1;
}

void CameraManager::start()
{
    if (startNum == 1) return;
    startNum = 1;

    if (inputType == 2)
    {
        emit camStartSignal();
    }
}

void CameraManager::stop()
{
    if (startNum == 0) return;
    startNum = 0;

    if (inputType == 2)
    {
        emit camStopSignal();
    }
}

bool CameraManager::isStarted()
{
    return startNum != 0;
}

void CameraManager::camStart()
{
    if (camDevice)
        camDevice->start();
}

void CameraManager::camStop()
{
    if (camDevice)
        camDevice->stop();
}

void CameraManager::setXFlip(bool flip)
{
    xFlip = flip;
}

void CameraManager::captureFrame(u32* frame, int width, int height, bool yuv)
{
    frameMutex.lock();

    if ((width == frameWidth) &&
        (height == frameHeight) &&
        (yuv == frameFormatYUV) &&
        (!xFlip))
    {
        int len = width * height;
        if (yuv) len /= 2;
        memcpy(frame, frameBuffer, len * sizeof(u32));
    }
    else
    {
        if (yuv == frameFormatYUV)
        {
            copyFrame_Straight(frameBuffer, frameWidth, frameHeight,
                               frame, width, height,
                               xFlip, yuv);
        }
        else if (yuv)
        {
            copyFrame_RGBtoYUV(frameBuffer, frameWidth, frameHeight,
                               frame, width, height,
                               xFlip);
        }
        else
        {
            copyFrame_YUVtoRGB(frameBuffer, frameWidth, frameHeight,
                               frame, width, height,
                               xFlip);
        }
    }

    frameMutex.unlock();
}

void CameraManager::feedFrame(u32* frame, int width, int height, bool yuv)
{
    frameMutex.lock();

    if (width == frameWidth && height == frameHeight && yuv == frameFormatYUV)
    {
        int len = width * height;
        if (yuv) len /= 2;
        memcpy(frameBuffer, frame, len * sizeof(u32));
    }
    else
    {
        if (yuv == frameFormatYUV)
        {
            copyFrame_Straight(frame, width, height,
                               frameBuffer, frameWidth, frameHeight,
                               false, yuv);
        }
        else if (yuv)
        {
            copyFrame_RGBtoYUV(frame, width, height,
                               frameBuffer, frameWidth, frameHeight,
                               false);
        }
        else
        {
            copyFrame_YUVtoRGB(frame, width, height,
                               frameBuffer, frameWidth, frameHeight,
                               false);
        }
    }

    frameMutex.unlock();
}

void CameraManager::feedFrame_UYVY(u32* frame, int width, int height)
{
    for (int y = 0; y < frameHeight; y++)
    {
        int sy = (y * height) / frameHeight;

        for (int x = 0; x < frameWidth; x+=2)
        {
            int sx = (x * width) / frameWidth;

            u32 val = frame[((sy*width) + sx) >> 1];

            val = ((val & 0xFF00FF00) >> 8) | ((val & 0x00FF00FF) << 8);

            tempFrameBuffer[((y*frameWidth) + x) >> 1] = val;
        }
    }

    feedFrame(tempFrameBuffer, frameWidth, frameHeight, true);
}

void CameraManager::feedFrame_NV12(u8* planeY, u8* planeUV, int width, int height)
{
    for (int y = 0; y < frameHeight; y++)
    {
        int sy = (y * height) / frameHeight;

        for (int x = 0; x < frameWidth; x+=2)
        {
            int sx1 = (x * width) / frameWidth;
            int sx2 = ((x+1) * width) / frameWidth;

            u32 val;

            u8 y1 = planeY[(sy*width) + sx1];
            u8 y2 = planeY[(sy*width) + sx2];

            int uvpos = (((sy>>1)*(width>>1)) + (sx1>>1));
            u8 u = planeUV[uvpos << 1];
            u8 v = planeUV[(uvpos << 1) + 1];

            val = y1 | (u << 8) | (y2 << 16) | (v << 24);
            tempFrameBuffer[((y*frameWidth) + x) >> 1] = val;
        }
    }

    feedFrame(tempFrameBuffer, frameWidth, frameHeight, true);
}

void CameraManager::copyFrame_Straight(u32* src, int swidth, int sheight, u32* dst, int dwidth, int dheight, bool xflip, bool yuv)
{
    if (yuv)
    {
        swidth /= 2;
        dwidth /= 2;
    }

    for (int dy = 0; dy < dheight; dy++)
    {
        int sy = (dy * sheight) / dheight;

        for (int dx = 0; dx < dwidth; dx++)
        {
            int sx = (dx * swidth) / dwidth;
            if (xflip) sx = swidth-1 - sx;

            u32 val = src[(sy * swidth) + sx];

            if (yuv)
            {
                if (xflip)
                    val = (val & 0xFF00FF00) |
                          ((val >> 16) & 0xFF) |
                          ((val & 0xFF) << 16);
            }
            else
                val |= 0xFF000000;

            dst[(dy * dwidth) + dx] = val;
        }
    }
}

void CameraManager::copyFrame_RGBtoYUV(u32* src, int swidth, int sheight, u32* dst, int dwidth, int dheight, bool xflip)
{
    for (int dy = 0; dy < dheight; dy++)
    {
        int sy = (dy * sheight) / dheight;

        for (int dx = 0; dx < dwidth; dx+=2)
        {
            int sx;

            sx = (dx * swidth) / dwidth;
            if (xflip) sx = swidth-1 - sx;

            u32 pixel1 = src[sy*swidth + sx];

            sx = ((dx+1) * swidth) / dwidth;
            if (xflip) sx = swidth-1 - sx;

            u32 pixel2 = src[sy*swidth + sx];

            int r1 = (pixel1 >> 16) & 0xFF;
            int g1 = (pixel1 >> 8) & 0xFF;
            int b1 = pixel1 & 0xFF;

            int r2 = (pixel2 >> 16) & 0xFF;
            int g2 = (pixel2 >> 8) & 0xFF;
            int b2 = pixel2 & 0xFF;

            int y1 = ((r1 * 19595) + (g1 * 38470) + (b1 * 7471)) >> 16;
            int u1 = ((b1 - y1) * 32244) >> 16;
            int v1 = ((r1 - y1) * 57475) >> 16;

            int y2 = ((r2 * 19595) + (g2 * 38470) + (b2 * 7471)) >> 16;
            int u2 = ((b2 - y2) * 32244) >> 16;
            int v2 = ((r2 - y2) * 57475) >> 16;

            u1 += 128; v1 += 128;
            u2 += 128; v2 += 128;

            y1 = std::clamp(y1, 0, 255); u1 = std::clamp(u1, 0, 255); v1 = std::clamp(v1, 0, 255);
            y2 = std::clamp(y2, 0, 255); u2 = std::clamp(u2, 0, 255); v2 = std::clamp(v2, 0, 255);

            // huh
            u1 = (u1 + u2) >> 1;
            v1 = (v1 + v2) >> 1;

            dst[(dy*dwidth + dx) / 2] = y1 | (u1 << 8) | (y2 << 16) | (v1 << 24);
        }
    }
}

void CameraManager::copyFrame_YUVtoRGB(u32* src, int swidth, int sheight, u32* dst, int dwidth, int dheight, bool xflip)
{
    for (int dy = 0; dy < dheight; dy++)
    {
        int sy = (dy * sheight) / dheight;

        for (int dx = 0; dx < dwidth; dx+=2)
        {
            int sx = (dx * swidth) / dwidth;
            if (xflip) sx = swidth-2 - sx;

            u32 val = src[(sy*swidth + sx) / 2];

            int y1, y2;
            if (xflip)
            {
                y1 = (val >> 16) & 0xFF;
                y2 = val & 0xFF;
            }
            else
            {
                y1 = val & 0xFF;
                y2 = (val >> 16) & 0xFF;
            }
            int u = (val >> 8) & 0xFF;
            int v = (val >> 24) & 0xFF;

            u -= 128; v -= 128;

            int r1 = y1 + ((v * 91881) >> 16);
            int g1 = y1 - ((v * 46793) >> 16) - ((u * 22544) >> 16);
            int b1 = y1 + ((u * 116129) >> 16);

            int r2 = y2 + ((v * 91881) >> 16);
            int g2 = y2 - ((v * 46793) >> 16) - ((u * 22544) >> 16);
            int b2 = y2 + ((u * 116129) >> 16);

            r1 = std::clamp(r1, 0, 255); g1 = std::clamp(g1, 0, 255); b1 = std::clamp(b1, 0, 255);
            r2 = std::clamp(r2, 0, 255); g2 = std::clamp(g2, 0, 255); b2 = std::clamp(b2, 0, 255);

            u32 col1 = 0xFF000000 | (r1 << 16) | (g1 << 8) | b1;
            u32 col2 = 0xFF000000 | (r2 << 16) | (g2 << 8) | b2;

            dst[dy*dwidth + dx  ] = col1;
            dst[dy*dwidth + dx+1] = col2;
        }
    }
}
