/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtCore/qdebug.h>
#include <QFile>
#include <QtConcurrent/QtConcurrentRun>
#include <QtMultimedia/qabstractvideobuffer.h>
#include <QtMultimedia/qvideosurfaceformat.h>
#include <QtMultimedia/qcameraimagecapture.h>
#include <private/qmemoryvideobuffer_p.h>

#include "dscamerasession.h"
#include "dsvideorenderer.h"
#include "directshowsamplegrabber.h"
#include "directshowcameraglobal.h"
#include "directshowmediatype.h"
#include "directshowutils.h"
#include "directshowvideoprobecontrol.h"

QT_BEGIN_NAMESPACE

DSCameraSession::DSCameraSession(QObject *parent)
    : QObject(parent)
{
    connect(this, &DSCameraSession::statusChanged,
            this, &DSCameraSession::updateReadyForCapture);

    m_deviceLostEventTimer.setSingleShot(true);
    connect(&m_deviceLostEventTimer, &QTimer::timeout, [&]() {
        IMediaEvent *pEvent = com_cast<IMediaEvent>(m_filterGraph, IID_IMediaEvent);
        if (!pEvent)
            return;

        long eventCode;
        LONG_PTR param1;
        LONG_PTR param2;
        while (pEvent->GetEvent(&eventCode, &param1, &param2, 0) == S_OK) {
            switch (eventCode) {
            case EC_DEVICE_LOST:
                unload();
                break;
            default:
                break;
            }

            pEvent->FreeEventParams(eventCode, param1, param2);
        }

        pEvent->Release();
    });
}

DSCameraSession::~DSCameraSession()
{
    unload();
}

void DSCameraSession::setSurface(QAbstractVideoSurface* surface)
{
    m_surface = surface;
}

void DSCameraSession::setDevice(const QString &device)
{
    m_sourceDeviceName = device;
}

QCameraViewfinderSettings DSCameraSession::viewfinderSettings() const
{
    return m_status == QCamera::ActiveStatus ? m_actualViewfinderSettings : m_viewfinderSettings;
}

void DSCameraSession::setViewfinderSettings(const QCameraViewfinderSettings &settings)
{
    m_viewfinderSettings = settings;
}

qreal DSCameraSession::scaledImageProcessingParameterValue(
        const ImageProcessingParameterInfo &sourceValueInfo)
{
    if (sourceValueInfo.currentValue == sourceValueInfo.defaultValue)
        return 0.0f;
    if (sourceValueInfo.currentValue < sourceValueInfo.defaultValue) {
        return ((sourceValueInfo.currentValue - sourceValueInfo.minimumValue)
                / qreal(sourceValueInfo.defaultValue - sourceValueInfo.minimumValue))
                + (-1.0f);
    }
    return ((sourceValueInfo.currentValue - sourceValueInfo.defaultValue)
            / qreal(sourceValueInfo.maximumValue - sourceValueInfo.defaultValue));
}

qint32 DSCameraSession::sourceImageProcessingParameterValue(
        qreal scaledValue, const ImageProcessingParameterInfo &valueRange)
{
    if (qFuzzyIsNull(scaledValue))
        return valueRange.defaultValue;
    if (scaledValue < 0.0f) {
        return ((scaledValue - (-1.0f)) * (valueRange.defaultValue - valueRange.minimumValue))
                + valueRange.minimumValue;
    }
    return (scaledValue * (valueRange.maximumValue - valueRange.defaultValue))
            + valueRange.defaultValue;
}

static QCameraImageProcessingControl::ProcessingParameter searchRelatedResultingParameter(
        QCameraImageProcessingControl::ProcessingParameter sourceParameter)
{
    if (sourceParameter == QCameraImageProcessingControl::WhiteBalancePreset)
        return QCameraImageProcessingControl::ColorTemperature;
    return sourceParameter;
}

bool DSCameraSession::isImageProcessingParameterSupported(
        QCameraImageProcessingControl::ProcessingParameter parameter) const
{
    const QCameraImageProcessingControl::ProcessingParameter resultingParameter =
            searchRelatedResultingParameter(parameter);

    return m_imageProcessingParametersInfos.contains(resultingParameter);
}

bool DSCameraSession::isImageProcessingParameterValueSupported(
        QCameraImageProcessingControl::ProcessingParameter parameter,
        const QVariant &value) const
{
    const QCameraImageProcessingControl::ProcessingParameter resultingParameter =
            searchRelatedResultingParameter(parameter);

    QMap<QCameraImageProcessingControl::ProcessingParameter,
            ImageProcessingParameterInfo>::const_iterator sourceValueInfo =
            m_imageProcessingParametersInfos.constFind(resultingParameter);

    if (sourceValueInfo == m_imageProcessingParametersInfos.constEnd())
        return false;

    switch (parameter) {

    case QCameraImageProcessingControl::WhiteBalancePreset: {
        const QCameraImageProcessing::WhiteBalanceMode checkedValue =
                value.value<QCameraImageProcessing::WhiteBalanceMode>();
        // Supports only the Manual and the Auto values
        if (checkedValue != QCameraImageProcessing::WhiteBalanceManual
                && checkedValue != QCameraImageProcessing::WhiteBalanceAuto) {
            return false;
        }
    }
        break;

    case QCameraImageProcessingControl::ColorTemperature: {
        const qint32 checkedValue = value.toInt();
        if (checkedValue < (*sourceValueInfo).minimumValue
                || checkedValue > (*sourceValueInfo).maximumValue) {
            return false;
        }
    }
        break;

    case QCameraImageProcessingControl::ContrastAdjustment: // falling back
    case QCameraImageProcessingControl::SaturationAdjustment: // falling back
    case QCameraImageProcessingControl::BrightnessAdjustment: // falling back
    case QCameraImageProcessingControl::SharpeningAdjustment: {
        const qint32 sourceValue = sourceImageProcessingParameterValue(
                    value.toReal(), (*sourceValueInfo));
        if (sourceValue < (*sourceValueInfo).minimumValue
                || sourceValue > (*sourceValueInfo).maximumValue)
            return false;
    }
        break;

    default:
        return false;
    }

    return true;
}

QVariant DSCameraSession::imageProcessingParameter(
        QCameraImageProcessingControl::ProcessingParameter parameter) const
{
    if (!m_graphBuilder) {
        auto it = m_pendingImageProcessingParametrs.find(parameter);
        return it != m_pendingImageProcessingParametrs.end() ? it.value() : QVariant();
    }

    const QCameraImageProcessingControl::ProcessingParameter resultingParameter =
            searchRelatedResultingParameter(parameter);

    QMap<QCameraImageProcessingControl::ProcessingParameter,
            ImageProcessingParameterInfo>::const_iterator sourceValueInfo =
            m_imageProcessingParametersInfos.constFind(resultingParameter);

    if (sourceValueInfo == m_imageProcessingParametersInfos.constEnd())
        return QVariant();

    switch (parameter) {

    case QCameraImageProcessingControl::WhiteBalancePreset:
        return QVariant::fromValue<QCameraImageProcessing::WhiteBalanceMode>(
                    (*sourceValueInfo).capsFlags == VideoProcAmp_Flags_Auto
                    ? QCameraImageProcessing::WhiteBalanceAuto
                    : QCameraImageProcessing::WhiteBalanceManual);

    case QCameraImageProcessingControl::ColorTemperature:
        return QVariant::fromValue<qint32>((*sourceValueInfo).currentValue);

    case QCameraImageProcessingControl::ContrastAdjustment: // falling back
    case QCameraImageProcessingControl::SaturationAdjustment: // falling back
    case QCameraImageProcessingControl::BrightnessAdjustment: // falling back
    case QCameraImageProcessingControl::SharpeningAdjustment:
        return scaledImageProcessingParameterValue((*sourceValueInfo));

    default:
        return QVariant();
    }
}

void DSCameraSession::setImageProcessingParameter(
        QCameraImageProcessingControl::ProcessingParameter parameter,
        const QVariant &value)
{
    if (!m_graphBuilder) {
        m_pendingImageProcessingParametrs.insert(parameter, value);
        return;
    }

    const QCameraImageProcessingControl::ProcessingParameter resultingParameter =
            searchRelatedResultingParameter(parameter);

    QMap<QCameraImageProcessingControl::ProcessingParameter,
            ImageProcessingParameterInfo>::iterator sourceValueInfo =
            m_imageProcessingParametersInfos.find(resultingParameter);

    if (sourceValueInfo == m_imageProcessingParametersInfos.end())
        return;

    LONG sourceValue = 0;
    LONG capsFlags = VideoProcAmp_Flags_Manual;

    switch (parameter) {

    case QCameraImageProcessingControl::WhiteBalancePreset: {
        const QCameraImageProcessing::WhiteBalanceMode checkedValue =
                value.value<QCameraImageProcessing::WhiteBalanceMode>();
        // Supports only the Manual and the Auto values
        if (checkedValue == QCameraImageProcessing::WhiteBalanceManual)
            capsFlags = VideoProcAmp_Flags_Manual;
        else if (checkedValue == QCameraImageProcessing::WhiteBalanceAuto)
            capsFlags = VideoProcAmp_Flags_Auto;
        else
            return;

        sourceValue = ((*sourceValueInfo).hasBeenExplicitlySet)
                ? (*sourceValueInfo).currentValue
                : (*sourceValueInfo).defaultValue;
    }
        break;

    case QCameraImageProcessingControl::ColorTemperature:
        sourceValue = value.isValid() ?
                    value.value<qint32>() : (*sourceValueInfo).defaultValue;
        capsFlags = (*sourceValueInfo).capsFlags;
        break;

    case QCameraImageProcessingControl::ContrastAdjustment: // falling back
    case QCameraImageProcessingControl::SaturationAdjustment: // falling back
    case QCameraImageProcessingControl::BrightnessAdjustment: // falling back
    case QCameraImageProcessingControl::SharpeningAdjustment:
        if (value.isValid()) {
            sourceValue = sourceImageProcessingParameterValue(
                        value.toReal(), (*sourceValueInfo));
        } else {
            sourceValue = (*sourceValueInfo).defaultValue;
        }
        break;

    default:
        return;
    }

    IAMVideoProcAmp *pVideoProcAmp = nullptr;
    HRESULT hr = m_graphBuilder->FindInterface(
                nullptr,
                nullptr,
                m_sourceFilter,
                IID_IAMVideoProcAmp,
                reinterpret_cast<void**>(&pVideoProcAmp)
                );

    if (FAILED(hr) || !pVideoProcAmp) {
        qWarning() << "failed to find the video proc amp";
        return;
    }

    hr = pVideoProcAmp->Set(
                (*sourceValueInfo).videoProcAmpProperty,
                sourceValue,
                capsFlags);

    pVideoProcAmp->Release();

    if (FAILED(hr)) {
        qWarning() << "failed to set the parameter value";
    } else {
        (*sourceValueInfo).capsFlags = capsFlags;
        (*sourceValueInfo).hasBeenExplicitlySet = true;
        (*sourceValueInfo).currentValue = sourceValue;
    }
}

bool DSCameraSession::getCameraControlInterface(IAMCameraControl **cameraControl) const
{
    if (!m_sourceFilter) {
        qCDebug(qtDirectShowPlugin, "getCameraControlInterface failed: No capture filter!");
        return false;
    }

    if (!cameraControl) {
        qCDebug(qtDirectShowPlugin, "getCameraControlInterface failed: Invalid out argument!");
        return false;
    }

    if (FAILED(m_sourceFilter->QueryInterface(IID_IAMCameraControl, reinterpret_cast<void **>(cameraControl)))) {
        qCDebug(qtDirectShowPlugin, "getCameraControlInterface failed: Querying camera control failed!");
        return false;
    }

    return true;
}

bool DSCameraSession::isCaptureDestinationSupported(QCameraImageCapture::CaptureDestinations destination) const
{
    return destination & (QCameraImageCapture::CaptureToFile | QCameraImageCapture::CaptureToBuffer);
}

QCameraImageCapture::CaptureDestinations DSCameraSession::captureDestination() const
{
    return m_captureDestinations;
}

void DSCameraSession::setCaptureDestination(QCameraImageCapture::CaptureDestinations destinations)
{
    if (m_captureDestinations == destinations)
        return;

    m_captureDestinations = destinations;
    Q_EMIT captureDestinationChanged(m_captureDestinations);
}

void DSCameraSession::addVideoProbe(DirectShowVideoProbeControl *probe)
{
    const QMutexLocker locker(&m_probeMutex);
    m_videoProbeControl = probe;
}

void DSCameraSession::removeVideoProbe(DirectShowVideoProbeControl *probe)
{
    Q_UNUSED(probe);
    Q_ASSERT(m_videoProbeControl == probe);
    const QMutexLocker locker(&m_probeMutex);
    m_videoProbeControl = nullptr;
}

bool DSCameraSession::load()
{
    unload();

    setStatus(QCamera::LoadingStatus);

    bool succeeded = createFilterGraph();
    if (succeeded)
        setStatus(QCamera::LoadedStatus);
    else
        setStatus(QCamera::UnavailableStatus);

    return succeeded;
}

bool DSCameraSession::unload()
{
    if (!m_graphBuilder)
        return false;

    if (!stopPreview())
        return false;

    setStatus(QCamera::UnloadingStatus);

    m_needsHorizontalMirroring = false;
    m_supportedViewfinderSettings.clear();
    m_supportedFormats.clear();
    SAFE_RELEASE(m_sourceFilter);
    SAFE_RELEASE(m_nullRendererFilter);
    SAFE_RELEASE(m_filterGraph);
    SAFE_RELEASE(m_graphBuilder);
    SAFE_RELEASE(m_outputPin);

    setStatus(QCamera::UnloadedStatus);

    return true;
}

bool DSCameraSession::startPreview()
{
    if (m_previewStarted)
        return true;

    if (!m_graphBuilder)
        return false;

    setStatus(QCamera::StartingStatus);

    QString errorString;
    HRESULT hr = S_OK;
    IMediaControl* pControl = nullptr;

    if (!configurePreviewFormat()) {
        errorString = tr("Failed to configure preview format");
        goto failed;
    }

    if (!connectGraph()) {
        errorString = tr("Failed to connect graph");
        goto failed;
    }

    if (m_surface)
        m_surface->start(m_previewSurfaceFormat);

    hr = m_filterGraph->QueryInterface(IID_IMediaControl, reinterpret_cast<void**>(&pControl));
    if (FAILED(hr)) {
        errorString = tr("Failed to get stream control");
        goto failed;
    }
    hr = pControl->Run();
    pControl->Release();

    if (FAILED(hr)) {
        errorString = tr("Failed to start");
        goto failed;
    }

    setStatus(QCamera::ActiveStatus);
    m_previewStarted = true;
    return true;

failed:
    // go back to a clean state
    if (m_surface && m_surface->isActive())
        m_surface->stop();
    disconnectGraph();
    setError(QCamera::CameraError, errorString, hr);
    return false;
}

bool DSCameraSession::stopPreview()
{
    if (!m_previewStarted)
        return true;

    setStatus(QCamera::StoppingStatus);

    if (m_previewSampleGrabber)
        m_previewSampleGrabber->stop();

    QString errorString;
    IMediaControl* pControl = nullptr;
    HRESULT hr = m_filterGraph->QueryInterface(IID_IMediaControl,
                                               reinterpret_cast<void**>(&pControl));
    if (FAILED(hr)) {
        errorString = tr("Failed to get stream control");
        goto failed;
    }

    hr = pControl->Stop();
    pControl->Release();
    if (FAILED(hr)) {
        errorString = tr("Failed to stop");
        goto failed;
    }

    disconnectGraph();

    m_sourceFormat.clear();

    m_previewStarted = false;
    setStatus(QCamera::LoadedStatus);
    return true;

failed:
    setError(QCamera::CameraError, errorString, hr);
    return false;
}

void DSCameraSession::setError(int error, const QString &errorString, HRESULT hr)
{
    qErrnoWarning(hr, "[0x%x] %s", hr, qPrintable(errorString));
    emit cameraError(error, errorString);
    setStatus(QCamera::UnloadedStatus);
}

void DSCameraSession::setStatus(QCamera::Status status)
{
    if (m_status == status)
        return;

    m_status = status;
    emit statusChanged(m_status);
}

bool DSCameraSession::isReadyForCapture()
{
    return m_readyForCapture;
}

void DSCameraSession::updateReadyForCapture()
{
    bool isReady = (m_status == QCamera::ActiveStatus && m_imageCaptureFileName.isEmpty());
    if (isReady != m_readyForCapture) {
        m_readyForCapture = isReady;
        emit readyForCaptureChanged(isReady);
    }
}

int DSCameraSession::captureImage(const QString &fileName)
{
    ++m_imageIdCounter;

    if (!m_readyForCapture) {
        emit captureError(m_imageIdCounter, QCameraImageCapture::NotReadyError,
                          tr("Camera not ready for capture"));
        return m_imageIdCounter;
    }

    const QString ext = !m_imageEncoderSettings.codec().isEmpty()
        ? m_imageEncoderSettings.codec().toLower()
        : QLatin1String("jpg");
    m_imageCaptureFileName = m_fileNameGenerator.generateFileName(fileName,
                                                         QMediaStorageLocation::Pictures,
                                                         QLatin1String("IMG_"),
                                                         ext);

    updateReadyForCapture();

    m_captureMutex.lock();
    m_currentImageId = m_imageIdCounter;
    m_captureMutex.unlock();

    return m_imageIdCounter;
}

void DSCameraSession::onFrameAvailable(double time, const QByteArray &data)
{
    // !!! Not called on the main thread
    Q_UNUSED(time);

    m_presentMutex.lock();

    // In case the source produces frames faster than we can display them,
    // only keep the most recent one
    m_currentFrame = QVideoFrame(new QMemoryVideoBuffer(data, m_stride),
                                 m_previewSize,
                                 m_previewPixelFormat);

    m_presentMutex.unlock();

    {
        const QMutexLocker locker(&m_probeMutex);
        if (m_currentFrame.isValid() && m_videoProbeControl)
            Q_EMIT m_videoProbeControl->videoFrameProbed(m_currentFrame);
    }

    // Image capture
    QMutexLocker locker(&m_captureMutex);
    if (m_currentImageId != -1 && !m_capturedFrame.isValid()) {
        m_capturedFrame = m_currentFrame;
        QMetaObject::invokeMethod(this, "imageExposed",  Qt::QueuedConnection, Q_ARG(int, m_currentImageId));
    }

    QMetaObject::invokeMethod(this, "presentFrame", Qt::QueuedConnection);
}

void DSCameraSession::presentFrame()
{
    // If no frames provided from ISampleGrabber for some time
    // the device might be potentially unplugged.
    m_deviceLostEventTimer.start(100);

    m_presentMutex.lock();

    if (m_currentFrame.isValid() && m_surface) {
        m_surface->present(m_currentFrame);
        m_currentFrame = QVideoFrame();
    }

    m_presentMutex.unlock();

    QImage captureImage;
    const int captureId = m_currentImageId;

    m_captureMutex.lock();

    if (m_capturedFrame.isValid()) {

        captureImage = m_capturedFrame.image();

        const bool needsVerticalMirroring = m_previewSurfaceFormat.scanLineDirection() != QVideoSurfaceFormat::TopToBottom;
        captureImage = captureImage.mirrored(m_needsHorizontalMirroring, needsVerticalMirroring); // also causes a deep copy of the data

        QtConcurrent::run(this, &DSCameraSession::processCapturedImage,
                          m_currentImageId, m_captureDestinations, captureImage, m_imageCaptureFileName);

        m_imageCaptureFileName.clear();
        m_currentImageId = -1;

        m_capturedFrame = QVideoFrame();
    }

    m_captureMutex.unlock();

    if (!captureImage.isNull())
        emit imageCaptured(captureId, captureImage);

    updateReadyForCapture();
}

void DSCameraSession::processCapturedImage(int id,
                                           QCameraImageCapture::CaptureDestinations captureDestinations,
                                           const QImage &image,
                                           const QString &path)
{
    const QString format = m_imageEncoderSettings.codec();
    if (captureDestinations & QCameraImageCapture::CaptureToFile) {
        if (image.save(path, !format.isEmpty() ? format.toUtf8().constData() : "JPG")) {
            Q_EMIT imageSaved(id, path);
        } else {
            Q_EMIT captureError(id, QCameraImageCapture::ResourceError,
                              tr("Could not save image to file."));
        }
    }

    if (captureDestinations & QCameraImageCapture::CaptureToBuffer)
        Q_EMIT imageAvailable(id, QVideoFrame(image));
}

bool DSCameraSession::createFilterGraph()
{
    // Previously containered in <qedit.h>.
    static const CLSID cLSID_NullRenderer = { 0xC1F400A4, 0x3F08, 0x11d3, { 0x9F, 0x0B, 0x00, 0x60, 0x08, 0x03, 0x9E, 0x37 } };

    QString errorString;
    HRESULT hr;
    IMoniker* pMoniker = nullptr;
    ICreateDevEnum* pDevEnum = nullptr;
    IEnumMoniker* pEnum = nullptr;

    // Create the filter graph
    hr = CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC,
                          IID_IGraphBuilder, reinterpret_cast<void**>(&m_filterGraph));
    if (FAILED(hr)) {
        errorString = tr("Failed to create filter graph");
        goto failed;
    }

    // Create the capture graph builder
    hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC,
                          IID_ICaptureGraphBuilder2,
                          reinterpret_cast<void**>(&m_graphBuilder));
    if (FAILED(hr)) {
        errorString = tr("Failed to create graph builder");
        goto failed;
    }

    // Attach the filter graph to the capture graph
    hr = m_graphBuilder->SetFiltergraph(m_filterGraph);
    if (FAILED(hr)) {
        errorString = tr("Failed to connect capture graph and filter graph");
        goto failed;
    }

    // Find the Capture device
    hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr,
                          CLSCTX_INPROC_SERVER, IID_ICreateDevEnum,
                          reinterpret_cast<void**>(&pDevEnum));
    if (SUCCEEDED(hr)) {
        // Create an enumerator for the video capture category
        hr = pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0);
        pDevEnum->Release();
        if (S_OK == hr) {
            pEnum->Reset();
            IMalloc *mallocInterface = nullptr;
            CoGetMalloc(1, (LPMALLOC*)&mallocInterface);
            //go through and find all video capture devices
            while (pEnum->Next(1, &pMoniker, nullptr) == S_OK) {

                BSTR strName = nullptr;
                hr = pMoniker->GetDisplayName(nullptr, nullptr, &strName);
                if (SUCCEEDED(hr)) {
                    QString output = QString::fromWCharArray(strName);
                    mallocInterface->Free(strName);
                    if (m_sourceDeviceName.contains(output)) {
                        hr = pMoniker->BindToObject(nullptr, nullptr, IID_IBaseFilter,
                                                    reinterpret_cast<void**>(&m_sourceFilter));
                        if (SUCCEEDED(hr)) {
                            pMoniker->Release();
                            break;
                        }
                    }
                }
                pMoniker->Release();
            }
            mallocInterface->Release();
            if (nullptr == m_sourceFilter)
            {
                if (m_sourceDeviceName.contains(QLatin1String("default")))
                {
                    pEnum->Reset();
                    // still have to loop to discard bind to storage failure case
                    while (pEnum->Next(1, &pMoniker, nullptr) == S_OK) {
                        IPropertyBag *pPropBag = nullptr;

                        hr = pMoniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag,
                                                     reinterpret_cast<void**>(&pPropBag));
                        if (FAILED(hr)) {
                            pMoniker->Release();
                            continue; // Don't panic yet
                        }

                        // No need to get the description, just grab it

                        hr = pMoniker->BindToObject(nullptr, nullptr, IID_IBaseFilter,
                                                    reinterpret_cast<void**>(&m_sourceFilter));
                        pPropBag->Release();
                        pMoniker->Release();
                        if (SUCCEEDED(hr))
                            break; // done, stop looping through
                        qWarning("Object bind failed");
                    }
                }
            }
            pEnum->Release();
        }
    }

    if (!m_sourceFilter) {
        errorString = tr("No capture device found");
        goto failed;
    }

    if (!DirectShowUtils::getPin(m_sourceFilter, PINDIR_OUTPUT, PIN_CATEGORY_CAPTURE, &m_outputPin, &hr))
        qWarning() << "Failed to get the pin for the video control:" << hr;

    // Sample grabber filter
    if (!m_previewSampleGrabber) {
        m_previewSampleGrabber = new DirectShowSampleGrabber(this);
        connect(m_previewSampleGrabber, &DirectShowSampleGrabber::bufferAvailable,
                this, &DSCameraSession::onFrameAvailable, Qt::DirectConnection);
    }


    // Null renderer. Input connected to the sample grabber's output. Simply
    // discard the samples it receives.
    hr = CoCreateInstance(cLSID_NullRenderer, nullptr, CLSCTX_INPROC,
                          IID_IBaseFilter, (void**)&m_nullRendererFilter);
    if (FAILED(hr)) {
        errorString = tr("Failed to create null renderer");
        goto failed;
    }

    updateSourceCapabilities();

    return true;

failed:
    m_needsHorizontalMirroring = false;
    SAFE_RELEASE(m_sourceFilter);
    SAFE_RELEASE(m_nullRendererFilter);
    SAFE_RELEASE(m_filterGraph);
    SAFE_RELEASE(m_graphBuilder);
    setError(QCamera::CameraError, errorString, hr);

    return false;
}

bool DSCameraSession::configurePreviewFormat()
{
    // Resolve viewfinder settings
    int settingsIndex = 0;
    const QSize captureResolution = m_imageEncoderSettings.resolution();
    const QSize resolution = captureResolution.isValid() ? captureResolution : m_viewfinderSettings.resolution();
    QCameraViewfinderSettings resolvedViewfinderSettings;
    for (const QCameraViewfinderSettings &s : qAsConst(m_supportedViewfinderSettings)) {
        if ((resolution.isEmpty() || resolution == s.resolution())
                && (qFuzzyIsNull(m_viewfinderSettings.minimumFrameRate()) || qFuzzyCompare((float)m_viewfinderSettings.minimumFrameRate(), (float)s.minimumFrameRate()))
                && (qFuzzyIsNull(m_viewfinderSettings.maximumFrameRate()) || qFuzzyCompare((float)m_viewfinderSettings.maximumFrameRate(), (float)s.maximumFrameRate()))
                && (m_viewfinderSettings.pixelFormat() == QVideoFrame::Format_Invalid || m_viewfinderSettings.pixelFormat() == s.pixelFormat())
                && (m_viewfinderSettings.pixelAspectRatio().isEmpty() || m_viewfinderSettings.pixelAspectRatio() == s.pixelAspectRatio())) {
            resolvedViewfinderSettings = s;
            break;
        }
        ++settingsIndex;
    }

    if (resolvedViewfinderSettings.isNull()) {
        qWarning("Invalid viewfinder settings");
        return false;
    }

    m_actualViewfinderSettings = resolvedViewfinderSettings;

    m_sourceFormat = m_supportedFormats[settingsIndex];
    // Set frame rate.
    // We don't care about the minimumFrameRate, DirectShow only allows to set an
    // average frame rate, so set that to the maximumFrameRate.
    VIDEOINFOHEADER *videoInfo = reinterpret_cast<VIDEOINFOHEADER*>(m_sourceFormat->pbFormat);
    videoInfo->AvgTimePerFrame = 10000000 / resolvedViewfinderSettings.maximumFrameRate();

    m_previewPixelFormat = resolvedViewfinderSettings.pixelFormat();
    const AM_MEDIA_TYPE *resolvedGrabberFormat = &m_sourceFormat;

    if (m_surface) {
        const auto surfaceFormats = m_surface->supportedPixelFormats(QAbstractVideoBuffer::NoHandle);
        if (!surfaceFormats.contains(m_previewPixelFormat)) {
            if (surfaceFormats.contains(QVideoFrame::Format_RGB32)) {
                // As a fallback, we support RGB32, if the capture source doesn't support
                // that format, the graph builder will automatically insert a
                // converter (when possible).

                static const AM_MEDIA_TYPE rgb32GrabberFormat { MEDIATYPE_Video, MEDIASUBTYPE_ARGB32, 0, 0, 0, FORMAT_VideoInfo, nullptr, 0, nullptr};
                resolvedGrabberFormat = &rgb32GrabberFormat;
                m_previewPixelFormat = QVideoFrame::Format_RGB32;

            } else {
                qWarning() << "Video surface needs to support at least RGB32 pixel format";
                return false;
            }
        }
    }

    m_previewSize = resolvedViewfinderSettings.resolution();
    m_previewSurfaceFormat = QVideoSurfaceFormat(m_previewSize,
                                                 m_previewPixelFormat,
                                                 QAbstractVideoBuffer::NoHandle);
    m_previewSurfaceFormat.setScanLineDirection(DirectShowMediaType::scanLineDirection(m_previewPixelFormat, videoInfo->bmiHeader));
    m_stride = DirectShowMediaType::bytesPerLine(m_previewSurfaceFormat);

    HRESULT hr;
    IAMStreamConfig* pConfig = nullptr;
    hr = m_graphBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,
                                       m_sourceFilter, IID_IAMStreamConfig,
                                       reinterpret_cast<void**>(&pConfig));
    if (FAILED(hr)) {
        qWarning() << "Failed to get config for capture device";
        return false;
    }

    hr = pConfig->SetFormat(&m_sourceFormat);

    pConfig->Release();

    if (FAILED(hr)) {
        qWarning() << "Unable to set video format on capture device";
        return false;
    }

    if (!m_previewSampleGrabber->setMediaType(resolvedGrabberFormat))
        return false;

    m_previewSampleGrabber->start(DirectShowSampleGrabber::CallbackMethod::BufferCB);

    return true;
}

void DSCameraSession::updateImageProcessingParametersInfos()
{
    if (!m_graphBuilder) {
        qWarning() << "failed to access to the graph builder";
        return;
    }

    IAMVideoProcAmp *pVideoProcAmp = nullptr;
    const HRESULT hr = m_graphBuilder->FindInterface(
                nullptr,
                nullptr,
                m_sourceFilter,
                IID_IAMVideoProcAmp,
                reinterpret_cast<void**>(&pVideoProcAmp)
                );

    if (FAILED(hr) || !pVideoProcAmp) {
        qWarning() << "failed to find the video proc amp";
        return;
    }

    for (int property = VideoProcAmp_Brightness; property <= VideoProcAmp_Gain; ++property) {

        QCameraImageProcessingControl::ProcessingParameter processingParameter; // not initialized

        switch (property) {
        case VideoProcAmp_Brightness:
            processingParameter = QCameraImageProcessingControl::BrightnessAdjustment;
            break;
        case VideoProcAmp_Contrast:
            processingParameter = QCameraImageProcessingControl::ContrastAdjustment;
            break;
        case VideoProcAmp_Saturation:
            processingParameter = QCameraImageProcessingControl::SaturationAdjustment;
            break;
        case VideoProcAmp_Sharpness:
            processingParameter = QCameraImageProcessingControl::SharpeningAdjustment;
            break;
        case VideoProcAmp_WhiteBalance:
            processingParameter = QCameraImageProcessingControl::ColorTemperature;
            break;
        default: // unsupported or not implemented yet parameter
            continue;
        }

        ImageProcessingParameterInfo sourceValueInfo;
        LONG steppingDelta = 0;

        HRESULT hr = pVideoProcAmp->GetRange(
                    property,
                    &sourceValueInfo.minimumValue,
                    &sourceValueInfo.maximumValue,
                    &steppingDelta,
                    &sourceValueInfo.defaultValue,
                    &sourceValueInfo.capsFlags);

        if (FAILED(hr))
            continue;

        hr = pVideoProcAmp->Get(
                    property,
                    &sourceValueInfo.currentValue,
                    &sourceValueInfo.capsFlags);

        if (FAILED(hr))
            continue;

        sourceValueInfo.videoProcAmpProperty = static_cast<VideoProcAmpProperty>(property);

        m_imageProcessingParametersInfos.insert(processingParameter, sourceValueInfo);
    }

    pVideoProcAmp->Release();

    for (auto it = m_pendingImageProcessingParametrs.cbegin();
        it != m_pendingImageProcessingParametrs.cend();
        ++it) {
        setImageProcessingParameter(it.key(), it.value());
    }
    m_pendingImageProcessingParametrs.clear();
}

bool DSCameraSession::connectGraph()
{
    HRESULT hr = m_filterGraph->AddFilter(m_sourceFilter, L"Capture Filter");
    if (FAILED(hr)) {
        qWarning() << "failed to add capture filter to graph";
        return false;
    }

    if (FAILED(m_filterGraph->AddFilter(m_previewSampleGrabber->filter(), L"Sample Grabber"))) {
        qWarning() << "failed to add sample grabber to graph";
        return false;
    }

    hr = m_filterGraph->AddFilter(m_nullRendererFilter, L"Null Renderer");
    if (FAILED(hr)) {
        qWarning() << "failed to add null renderer to graph";
        return false;
    }

    hr = m_graphBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,
                                      m_sourceFilter,
                                      m_previewSampleGrabber->filter(),
                                      m_nullRendererFilter);
    if (FAILED(hr)) {
        qWarning() << "Graph failed to connect filters" << hr;
        return false;
    }

    return true;
}

void DSCameraSession::disconnectGraph()
{
    // To avoid increasing the memory usage every time the graph is re-connected it's
    // important that all filters are released; also the ones added by the "Intelligent Connect".
    IEnumFilters *enumFilters = nullptr;
    if (SUCCEEDED(m_filterGraph->EnumFilters(&enumFilters)))  {
        IBaseFilter *filter = nullptr;
        while (enumFilters->Next(1, &filter, nullptr) == S_OK) {
                m_filterGraph->RemoveFilter(filter);
                enumFilters->Reset();
                filter->Release();
        }
        enumFilters->Release();
    }
}

static bool qt_frameRateRangeGreaterThan(const QCamera::FrameRateRange &r1, const QCamera::FrameRateRange &r2)
{
    return r1.maximumFrameRate > r2.maximumFrameRate;
}

void DSCameraSession::updateSourceCapabilities()
{
    HRESULT hr;
    AM_MEDIA_TYPE *pmt = nullptr;
    VIDEOINFOHEADER *pvi = nullptr;
    VIDEO_STREAM_CONFIG_CAPS scc;
    IAMStreamConfig* pConfig = nullptr;

    m_supportedViewfinderSettings.clear();
    m_needsHorizontalMirroring = false;
    m_supportedFormats.clear();
    m_imageProcessingParametersInfos.clear();

    IAMVideoControl *pVideoControl = nullptr;
    hr = m_graphBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,
                                       m_sourceFilter, IID_IAMVideoControl,
                                       reinterpret_cast<void**>(&pVideoControl));
    if (FAILED(hr)) {
        qWarning() << "Failed to get the video control";
    } else if (m_outputPin) {
        long supportedModes;
        hr = pVideoControl->GetCaps(m_outputPin, &supportedModes);
        if (FAILED(hr)) {
            qWarning() << "Failed to get the supported modes of the video control";
        } else if (supportedModes & VideoControlFlag_FlipHorizontal) {
            long mode;
            hr = pVideoControl->GetMode(m_outputPin, &mode);
            if (FAILED(hr))
                qWarning() << "Failed to get the mode of the video control";
            else if (supportedModes & VideoControlFlag_FlipHorizontal)
                m_needsHorizontalMirroring = (mode & VideoControlFlag_FlipHorizontal);
        }
        pVideoControl->Release();
    }

    hr = m_graphBuilder->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,
                                       m_sourceFilter, IID_IAMStreamConfig,
                                       reinterpret_cast<void**>(&pConfig));
    if (FAILED(hr)) {
        qWarning() << "failed to get config on capture device";
        return;
    }

    int iCount;
    int iSize;
    hr = pConfig->GetNumberOfCapabilities(&iCount, &iSize);
    if (FAILED(hr)) {
        qWarning() << "failed to get capabilities";
        return;
    }

    for (int iIndex = 0; iIndex < iCount; ++iIndex) {
        hr = pConfig->GetStreamCaps(iIndex, &pmt, reinterpret_cast<BYTE*>(&scc));
        if (hr == S_OK) {
            QVideoFrame::PixelFormat pixelFormat = DirectShowMediaType::pixelFormatFromType(pmt);

            if (pmt->majortype == MEDIATYPE_Video
                    && pmt->formattype == FORMAT_VideoInfo
                    && pixelFormat != QVideoFrame::Format_Invalid) {

                pvi = reinterpret_cast<VIDEOINFOHEADER*>(pmt->pbFormat);
                QSize resolution(pvi->bmiHeader.biWidth, pvi->bmiHeader.biHeight);

                QList<QCamera::FrameRateRange> frameRateRanges;

                if (pVideoControl && m_outputPin) {
                    long listSize = 0;
                    LONGLONG *frameRates = nullptr;
                    SIZE size = { resolution.width(), resolution.height() };
                    hr = pVideoControl->GetFrameRateList(m_outputPin, iIndex, size, &listSize, &frameRates);
                    if (hr == S_OK && listSize > 0 && frameRates) {
                        for (long i = 0; i < listSize; ++i) {
                            qreal fr = qreal(10000000) / frameRates[i];
                            frameRateRanges.append(QCamera::FrameRateRange(fr, fr));
                        }

                        // Make sure higher frame rates come first
                        std::sort(frameRateRanges.begin(), frameRateRanges.end(), qt_frameRateRangeGreaterThan);
                    }

                    CoTaskMemFree(frameRates);
                }

                if (frameRateRanges.isEmpty()) {
                    frameRateRanges.append(QCamera::FrameRateRange(qreal(10000000) / scc.MaxFrameInterval,
                                                                   qreal(10000000) / scc.MinFrameInterval));
                }

                for (const QCamera::FrameRateRange &frameRateRange : qAsConst(frameRateRanges)) {
                    QCameraViewfinderSettings settings;
                    settings.setResolution(resolution);
                    settings.setMinimumFrameRate(frameRateRange.minimumFrameRate);
                    settings.setMaximumFrameRate(frameRateRange.maximumFrameRate);
                    settings.setPixelFormat(pixelFormat);
                    settings.setPixelAspectRatio(1, 1);
                    m_supportedViewfinderSettings.append(settings);
                    m_supportedFormats.append(DirectShowMediaType(*pmt));
                }
            } else {
                OLECHAR *guidString = nullptr;
                StringFromCLSID(pmt->subtype, &guidString);
                if (guidString)
                    qWarning() << "Unsupported media type:" << QString::fromWCharArray(guidString);
                ::CoTaskMemFree(guidString);
            }

            DirectShowMediaType::deleteType(pmt);
        }
    }

    pConfig->Release();

    updateImageProcessingParametersInfos();
}

QList<QSize> DSCameraSession::supportedResolutions(bool *continuous) const
{
    if (continuous)
        *continuous = false;

    QList<QSize> res;
    for (auto &settings : m_supportedViewfinderSettings) {
        auto size = settings.resolution();
        if (!res.contains(size))
            res << size;
    }

    std::sort(res.begin(), res.end(), [](const QSize &r1, const QSize &r2) {
        return qlonglong(r1.width()) * r1.height() < qlonglong(r2.width()) * r2.height();
    });

    return res;
}

QT_END_NAMESPACE
