/****************************************************************************
**
** Copyright (C) 2013 Research In Motion
** Contact: http://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** 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 http://www.qt.io/terms-conditions. For further
** information use the contact form at http://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 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "bbcamerasession.h"

#include "bbcameraorientationhandler.h"
#include "bbcameraviewfindersettingscontrol.h"
#include "windowgrabber.h"

#include <QAbstractVideoSurface>
#include <QBuffer>
#include <QDebug>
#include <QImage>
#include <QUrl>
#include <QVideoSurfaceFormat>
#include <qmath.h>

#include <algorithm>

QT_BEGIN_NAMESPACE

static QString errorToString(camera_error_t error)
{
    switch (error) {
    case CAMERA_EOK:
        return QLatin1String("No error");
    case CAMERA_EAGAIN:
        return QLatin1String("Camera unavailable");
    case CAMERA_EINVAL:
        return QLatin1String("Invalid argument");
    case CAMERA_ENODEV:
        return QLatin1String("Camera not found");
    case CAMERA_EMFILE:
        return QLatin1String("File table overflow");
    case CAMERA_EBADF:
        return QLatin1String("Invalid handle passed");
    case CAMERA_EACCESS:
        return QLatin1String("No permission");
    case CAMERA_EBADR:
        return QLatin1String("Invalid file descriptor");
    case CAMERA_ENOENT:
        return QLatin1String("File or directory does not exists");
    case CAMERA_ENOMEM:
        return QLatin1String("Memory allocation failed");
    case CAMERA_EOPNOTSUPP:
        return QLatin1String("Operation not supported");
    case CAMERA_ETIMEDOUT:
        return QLatin1String("Communication timeout");
    case CAMERA_EALREADY:
        return QLatin1String("Operation already in progress");
    case CAMERA_EUNINIT:
        return QLatin1String("Camera library not initialized");
    case CAMERA_EREGFAULT:
        return QLatin1String("Callback registration failed");
    case CAMERA_EMICINUSE:
        return QLatin1String("Microphone in use already");
    case CAMERA_ENODATA:
        return QLatin1String("Data does not exist");
    case CAMERA_EBUSY:
        return QLatin1String("Camera busy");
    case CAMERA_EDESKTOPCAMERAINUSE:
        return QLatin1String("Desktop camera in use already");
    case CAMERA_ENOSPC:
        return QLatin1String("Disk is full");
    case CAMERA_EPOWERDOWN:
        return QLatin1String("Camera in power down state");
    case CAMERA_3ALOCKED:
        return QLatin1String("3A have been locked");
//  case CAMERA_EVIEWFINDERFROZEN: // not yet available in 10.2 NDK
//      return QLatin1String("Freeze flag set");
    default:
        return QLatin1String("Unknown error");
    }
}

QDebug operator<<(QDebug debug, camera_error_t error)
{
    debug.nospace() << errorToString(error);
    return debug.space();
}

BbCameraSession::BbCameraSession(QObject *parent)
    : QObject(parent)
    , m_nativeCameraOrientation(0)
    , m_orientationHandler(new BbCameraOrientationHandler(this))
    , m_status(QCamera::UnloadedStatus)
    , m_state(QCamera::UnloadedState)
    , m_captureMode(QCamera::CaptureStillImage)
    , m_device("bb:RearCamera")
    , m_previewIsVideo(true)
    , m_surface(0)
    , m_captureImageDriveMode(QCameraImageCapture::SingleImageCapture)
    , m_lastImageCaptureId(0)
    , m_captureDestination(QCameraImageCapture::CaptureToFile)
    , m_videoState(QMediaRecorder::StoppedState)
    , m_videoStatus(QMediaRecorder::LoadedStatus)
    , m_handle(CAMERA_HANDLE_INVALID)
    , m_windowGrabber(new WindowGrabber(this))
{
    connect(this, SIGNAL(statusChanged(QCamera::Status)), SLOT(updateReadyForCapture()));
    connect(this, SIGNAL(captureModeChanged(QCamera::CaptureModes)), SLOT(updateReadyForCapture()));
    connect(m_orientationHandler, SIGNAL(orientationChanged(int)), SLOT(deviceOrientationChanged(int)));

    connect(m_windowGrabber, SIGNAL(frameGrabbed(QImage, int)), SLOT(viewfinderFrameGrabbed(QImage)));
}

BbCameraSession::~BbCameraSession()
{
    stopViewFinder();
    closeCamera();
}

camera_handle_t BbCameraSession::handle() const
{
    return m_handle;
}

QCamera::State BbCameraSession::state() const
{
    return m_state;
}

void BbCameraSession::setState(QCamera::State state)
{
    if (m_state == state)
        return;

    const QCamera::State previousState = m_state;

    if (previousState == QCamera::UnloadedState) {
        if (state == QCamera::LoadedState) {
            if (openCamera()) {
                m_state = state;
            }
        } else if (state == QCamera::ActiveState) {
            if (openCamera()) {
                QMetaObject::invokeMethod(this, "applyConfiguration", Qt::QueuedConnection);
                m_state = state;
            }
        }
    } else if (previousState == QCamera::LoadedState) {
        if (state == QCamera::UnloadedState) {
            closeCamera();
            m_state = state;
        } else if (state == QCamera::ActiveState) {
            QMetaObject::invokeMethod(this, "applyConfiguration", Qt::QueuedConnection);
            m_state = state;
        }
    } else if (previousState == QCamera::ActiveState) {
        if (state == QCamera::LoadedState) {
            stopViewFinder();
            m_state = state;
        } else if (state == QCamera::UnloadedState) {
            stopViewFinder();
            closeCamera();
            m_state = state;
        }
    }

    if (m_state != previousState)
        emit stateChanged(m_state);
}

QCamera::Status BbCameraSession::status() const
{
    return m_status;
}

QCamera::CaptureModes BbCameraSession::captureMode() const
{
    return m_captureMode;
}

void BbCameraSession::setCaptureMode(QCamera::CaptureModes captureMode)
{
    if (m_captureMode == captureMode)
        return;

    m_captureMode = captureMode;
    emit captureModeChanged(m_captureMode);
}

bool BbCameraSession::isCaptureModeSupported(QCamera::CaptureModes mode) const
{
    if (m_handle == CAMERA_HANDLE_INVALID) {
        // the camera has not been loaded yet via QCamera::load(), so
        // we open it temporarily to peek for the supported capture modes

        camera_unit_t unit = CAMERA_UNIT_REAR;
        if (m_device == cameraIdentifierFront())
            unit = CAMERA_UNIT_FRONT;
        else if (m_device == cameraIdentifierRear())
            unit = CAMERA_UNIT_REAR;
        else if (m_device == cameraIdentifierDesktop())
            unit = CAMERA_UNIT_DESKTOP;

        camera_handle_t handle;
        const camera_error_t result = camera_open(unit, CAMERA_MODE_RW, &handle);
        if (result != CAMERA_EOK)
            return true;

        const bool supported = isCaptureModeSupported(handle, mode);

        camera_close(handle);

        return supported;
    } else {
        return isCaptureModeSupported(m_handle, mode);
    }
}

QByteArray BbCameraSession::cameraIdentifierFront()
{
    return "bb:FrontCamera";
}

QByteArray BbCameraSession::cameraIdentifierRear()
{
    return "bb:RearCamera";
}

QByteArray BbCameraSession::cameraIdentifierDesktop()
{
    return "bb:DesktopCamera";
}

void BbCameraSession::setDevice(const QByteArray &device)
{
    m_device = device;
}

QByteArray BbCameraSession::device() const
{
    return m_device;
}

QAbstractVideoSurface* BbCameraSession::surface() const
{
    return m_surface;
}

void BbCameraSession::setSurface(QAbstractVideoSurface *surface)
{
    QMutexLocker locker(&m_surfaceMutex);

    if (m_surface == surface)
        return;

    m_surface = surface;
}

bool BbCameraSession::isReadyForCapture() const
{
    if (m_captureMode & QCamera::CaptureStillImage)
        return (m_status == QCamera::ActiveStatus);

    if (m_captureMode & QCamera::CaptureVideo)
        return (m_status == QCamera::ActiveStatus);

    return false;
}

QCameraImageCapture::DriveMode BbCameraSession::driveMode() const
{
    return m_captureImageDriveMode;
}

void BbCameraSession::setDriveMode(QCameraImageCapture::DriveMode mode)
{
    m_captureImageDriveMode = mode;
}

/**
 * A helper structure that keeps context data for image capture callbacks.
 */
struct ImageCaptureData
{
    int requestId;
    QString fileName;
    BbCameraSession *session;
};

static void imageCaptureShutterCallback(camera_handle_t handle, void *context)
{
    Q_UNUSED(handle)

    const ImageCaptureData *data = static_cast<ImageCaptureData*>(context);

    // We are inside a worker thread here, so emit imageExposed inside the main thread
    QMetaObject::invokeMethod(data->session, "imageExposed", Qt::QueuedConnection,
                              Q_ARG(int, data->requestId));
}

static void imageCaptureImageCallback(camera_handle_t handle, camera_buffer_t *buffer, void *context)
{
    Q_UNUSED(handle)

    QScopedPointer<ImageCaptureData> data(static_cast<ImageCaptureData*>(context));

    if (buffer->frametype != CAMERA_FRAMETYPE_JPEG) {
        // We are inside a worker thread here, so emit error signal inside the main thread
        QMetaObject::invokeMethod(data->session, "imageCaptureError", Qt::QueuedConnection,
                                  Q_ARG(int, data->requestId),
                                  Q_ARG(QCameraImageCapture::Error, QCameraImageCapture::FormatError),
                                  Q_ARG(QString, BbCameraSession::tr("Camera provides image in unsupported format")));
        return;
    }

    const QByteArray rawData((const char*)buffer->framebuf, buffer->framedesc.jpeg.bufsize);

    QImage image;
    const bool ok = image.loadFromData(rawData, "JPG");
    if (!ok) {
        const QString errorMessage = BbCameraSession::tr("Could not load JPEG data from frame");
        // We are inside a worker thread here, so emit error signal inside the main thread
        QMetaObject::invokeMethod(data->session, "imageCaptureError", Qt::QueuedConnection,
                                  Q_ARG(int, data->requestId),
                                  Q_ARG(QCameraImageCapture::Error, QCameraImageCapture::FormatError),
                                  Q_ARG(QString, errorMessage));
        return;
    }


    // We are inside a worker thread here, so invoke imageCaptured inside the main thread
    QMetaObject::invokeMethod(data->session, "imageCaptured", Qt::QueuedConnection,
                              Q_ARG(int, data->requestId),
                              Q_ARG(QImage, image),
                              Q_ARG(QString, data->fileName));
}

int BbCameraSession::capture(const QString &fileName)
{
    m_lastImageCaptureId++;

    if (!isReadyForCapture()) {
        emit imageCaptureError(m_lastImageCaptureId, QCameraImageCapture::NotReadyError, tr("Camera not ready"));
        return m_lastImageCaptureId;
    }

    if (m_captureImageDriveMode == QCameraImageCapture::SingleImageCapture) {
        // prepare context object for callback
        ImageCaptureData *context = new ImageCaptureData;
        context->requestId = m_lastImageCaptureId;
        context->fileName = fileName;
        context->session = this;

        const camera_error_t result = camera_take_photo(m_handle,
                                                        imageCaptureShutterCallback,
                                                        0,
                                                        0,
                                                        imageCaptureImageCallback,
                                                        context, false);

        if (result != CAMERA_EOK)
            qWarning() << "Unable to take photo:" << result;
    } else {
        // TODO: implement burst mode when available in Qt API
    }

    return m_lastImageCaptureId;
}

void BbCameraSession::cancelCapture()
{
    // BB10 API doesn't provide functionality for that
}

bool BbCameraSession::isCaptureDestinationSupported(QCameraImageCapture::CaptureDestinations destination) const
{
    // capture to buffer, file and both are supported.
    return destination & (QCameraImageCapture::CaptureToFile | QCameraImageCapture::CaptureToBuffer);
}

QCameraImageCapture::CaptureDestinations BbCameraSession::captureDestination() const
{
    return m_captureDestination;
}

void BbCameraSession::setCaptureDestination(QCameraImageCapture::CaptureDestinations destination)
{
    if (m_captureDestination != destination) {
        m_captureDestination = destination;
        emit captureDestinationChanged(m_captureDestination);
    }
}

QList<QSize> BbCameraSession::supportedResolutions(const QImageEncoderSettings&, bool *continuous) const
{
    if (continuous)
        *continuous = false;

    if (m_status == QCamera::UnloadedStatus)
        return QList<QSize>();

    if (m_captureMode & QCamera::CaptureStillImage) {
        return supportedResolutions(QCamera::CaptureStillImage);
    } else if (m_captureMode & QCamera::CaptureVideo) {
        return supportedResolutions(QCamera::CaptureVideo);
    }

    return QList<QSize>();
}

QImageEncoderSettings BbCameraSession::imageSettings() const
{
    return m_imageEncoderSettings;
}

void BbCameraSession::setImageSettings(const QImageEncoderSettings &settings)
{
    m_imageEncoderSettings = settings;
    if (m_imageEncoderSettings.codec().isEmpty())
        m_imageEncoderSettings.setCodec(QLatin1String("jpeg"));
}

QUrl BbCameraSession::outputLocation() const
{
    return QUrl::fromLocalFile(m_videoOutputLocation);
}

bool BbCameraSession::setOutputLocation(const QUrl &location)
{
    m_videoOutputLocation = location.toLocalFile();

    return true;
}

QMediaRecorder::State BbCameraSession::videoState() const
{
    return m_videoState;
}

void BbCameraSession::setVideoState(QMediaRecorder::State state)
{
    if (m_videoState == state)
        return;

    const QMediaRecorder::State previousState = m_videoState;

    if (previousState == QMediaRecorder::StoppedState) {
        if (state == QMediaRecorder::RecordingState) {
            if (startVideoRecording()) {
                m_videoState = state;
            }
        } else if (state == QMediaRecorder::PausedState) {
            // do nothing
        }
    } else if (previousState == QMediaRecorder::RecordingState) {
        if (state == QMediaRecorder::StoppedState) {
            stopVideoRecording();
            m_videoState = state;
        } else if (state == QMediaRecorder::PausedState) {
            //TODO: (pause) not supported by BB10 API yet
        }
    } else if (previousState == QMediaRecorder::PausedState) {
        if (state == QMediaRecorder::StoppedState) {
            stopVideoRecording();
            m_videoState = state;
        } else if (state == QMediaRecorder::RecordingState) {
            //TODO: (resume) not supported by BB10 API yet
        }
    }

    emit videoStateChanged(m_videoState);
}

QMediaRecorder::Status BbCameraSession::videoStatus() const
{
    return m_videoStatus;
}

qint64 BbCameraSession::duration() const
{
    return (m_videoRecordingDuration.isValid() ? m_videoRecordingDuration.elapsed() : 0);
}

void BbCameraSession::applyVideoSettings()
{
    if (m_handle == CAMERA_HANDLE_INVALID)
        return;

    // apply viewfinder configuration
    const QList<QSize> videoOutputResolutions = supportedResolutions(QCamera::CaptureVideo);

    if (!m_videoEncoderSettings.resolution().isValid() || !videoOutputResolutions.contains(m_videoEncoderSettings.resolution()))
        m_videoEncoderSettings.setResolution(videoOutputResolutions.first());

    QSize viewfinderResolution;

    if (m_previewIsVideo) {
        // The viewfinder is responsible for encoding the video frames, so the resolutions must match.
        viewfinderResolution = m_videoEncoderSettings.resolution();
    } else {
        // The frames are encoded separately from the viewfinder, so only the aspect ratio must match.
        const QSize videoResolution = m_videoEncoderSettings.resolution();
        const qreal aspectRatio = static_cast<qreal>(videoResolution.width())/static_cast<qreal>(videoResolution.height());

        QList<QSize> sizes = supportedViewfinderResolutions(QCamera::CaptureVideo);
        std::reverse(sizes.begin(), sizes.end()); // use smallest possible resolution
        foreach (const QSize &size, sizes) {
            // search for viewfinder resolution with the same aspect ratio
            if (qFuzzyCompare(aspectRatio, (static_cast<qreal>(size.width())/static_cast<qreal>(size.height())))) {
                viewfinderResolution = size;
                break;
            }
        }
    }

    Q_ASSERT(viewfinderResolution.isValid());

    const QByteArray windowId = QString().sprintf("qcamera_vf_%p", this).toLatin1();
    m_windowGrabber->setWindowId(windowId);

    const QByteArray windowGroupId = m_windowGrabber->windowGroupId();

    const int rotationAngle = (360 - m_nativeCameraOrientation);

    camera_error_t result = CAMERA_EOK;
    result = camera_set_videovf_property(m_handle,
                                         CAMERA_IMGPROP_WIN_GROUPID, windowGroupId.data(),
                                         CAMERA_IMGPROP_WIN_ID, windowId.data(),
                                         CAMERA_IMGPROP_WIDTH, viewfinderResolution.width(),
                                         CAMERA_IMGPROP_HEIGHT, viewfinderResolution.height(),
                                         CAMERA_IMGPROP_ROTATION, rotationAngle);

    if (result != CAMERA_EOK) {
        qWarning() << "Unable to apply video viewfinder settings:" << result;
        return;
    }

    const QSize resolution = m_videoEncoderSettings.resolution();

    QString videoCodec = m_videoEncoderSettings.codec();
    if (videoCodec.isEmpty())
        videoCodec = QLatin1String("h264");

    camera_videocodec_t cameraVideoCodec = CAMERA_VIDEOCODEC_H264;
    if (videoCodec == QLatin1String("none"))
        cameraVideoCodec = CAMERA_VIDEOCODEC_NONE;
    else if (videoCodec == QLatin1String("avc1"))
        cameraVideoCodec = CAMERA_VIDEOCODEC_AVC1;
    else if (videoCodec == QLatin1String("h264"))
        cameraVideoCodec = CAMERA_VIDEOCODEC_H264;

    qreal frameRate = m_videoEncoderSettings.frameRate();
    if (frameRate == 0) {
        const QList<qreal> frameRates = supportedFrameRates(QVideoEncoderSettings(), 0);
        if (!frameRates.isEmpty())
            frameRate = frameRates.last();
    }

    QString audioCodec = m_audioEncoderSettings.codec();
    if (audioCodec.isEmpty())
        audioCodec = QLatin1String("aac");

    camera_audiocodec_t cameraAudioCodec = CAMERA_AUDIOCODEC_AAC;
    if (audioCodec == QLatin1String("none"))
        cameraAudioCodec = CAMERA_AUDIOCODEC_NONE;
    else if (audioCodec == QLatin1String("aac"))
        cameraAudioCodec = CAMERA_AUDIOCODEC_AAC;
    else if (audioCodec == QLatin1String("raw"))
        cameraAudioCodec = CAMERA_AUDIOCODEC_RAW;

    result = camera_set_video_property(m_handle,
                                       CAMERA_IMGPROP_WIDTH, resolution.width(),
                                       CAMERA_IMGPROP_HEIGHT, resolution.height(),
                                       CAMERA_IMGPROP_ROTATION, rotationAngle,
                                       CAMERA_IMGPROP_VIDEOCODEC, cameraVideoCodec,
                                       CAMERA_IMGPROP_AUDIOCODEC, cameraAudioCodec);

    if (result != CAMERA_EOK) {
        qWarning() << "Unable to apply video settings:" << result;
        emit videoError(QMediaRecorder::ResourceError, tr("Unable to apply video settings"));
    }
}

QList<QSize> BbCameraSession::supportedResolutions(const QVideoEncoderSettings &settings, bool *continuous) const
{
    Q_UNUSED(settings);

    if (continuous)
        *continuous = false;

    return supportedResolutions(QCamera::CaptureVideo);
}

QList<qreal> BbCameraSession::supportedFrameRates(const QVideoEncoderSettings &settings, bool *continuous) const
{
    Q_UNUSED(settings);

    if (m_handle == CAMERA_HANDLE_INVALID)
        return QList<qreal>();

    int supported = 0;
    double rates[20];
    bool maxmin = false;

    /**
     * Since in current version of the BB10 platform the video viewfinder encodes the video frames, we use
     * the values as returned by camera_get_video_vf_framerates().
     */
    const camera_error_t result = camera_get_video_vf_framerates(m_handle, 20, &supported, rates, &maxmin);
    if (result != CAMERA_EOK) {
        qWarning() << "Unable to retrieve supported viewfinder framerates:" << result;
        return QList<qreal>();
    }

    QList<qreal> frameRates;
    for (int i = 0; i < supported; ++i)
        frameRates << rates[i];

    if (continuous)
        *continuous = maxmin;

    return frameRates;
}

QVideoEncoderSettings BbCameraSession::videoSettings() const
{
    return m_videoEncoderSettings;
}

void BbCameraSession::setVideoSettings(const QVideoEncoderSettings &settings)
{
    m_videoEncoderSettings = settings;
}

QAudioEncoderSettings BbCameraSession::audioSettings() const
{
    return m_audioEncoderSettings;
}

void BbCameraSession::setAudioSettings(const QAudioEncoderSettings &settings)
{
    m_audioEncoderSettings = settings;
}

void BbCameraSession::updateReadyForCapture()
{
    emit readyForCaptureChanged(isReadyForCapture());
}

void BbCameraSession::imageCaptured(int requestId, const QImage &rawImage, const QString &fileName)
{
    QTransform transform;

    // subtract out the native rotation
    transform.rotate(m_nativeCameraOrientation);

    // subtract out the current device orientation
    if (m_device == cameraIdentifierRear())
        transform.rotate(360 - m_orientationHandler->orientation());
    else
        transform.rotate(m_orientationHandler->orientation());

    const QImage image = rawImage.transformed(transform);

    // Generate snap preview as downscaled image
    {
        QSize previewSize = image.size();
        int downScaleSteps = 0;
        while (previewSize.width() > 800 && downScaleSteps < 8) {
            previewSize.rwidth() /= 2;
            previewSize.rheight() /= 2;
            downScaleSteps++;
        }

        const QImage snapPreview = image.scaled(previewSize);

        emit imageCaptured(requestId, snapPreview);
    }

    if (m_captureDestination & QCameraImageCapture::CaptureToBuffer) {
        QVideoFrame frame(image);

        emit imageAvailable(requestId, frame);
    }

    if (m_captureDestination & QCameraImageCapture::CaptureToFile) {
        const QString actualFileName = m_mediaStorageLocation.generateFileName(fileName,
                                                                               QCamera::CaptureStillImage,
                                                                               QLatin1String("IMG_"),
                                                                               QLatin1String("jpg"));

        QFile file(actualFileName);
        if (file.open(QFile::WriteOnly)) {
            if (image.save(&file, "JPG")) {
                emit imageSaved(requestId, actualFileName);
            } else {
                emit imageCaptureError(requestId, QCameraImageCapture::OutOfSpaceError, file.errorString());
            }
        } else {
            const QString errorMessage = tr("Could not open destination file:\n%1").arg(actualFileName);
            emit imageCaptureError(requestId, QCameraImageCapture::ResourceError, errorMessage);
        }
    }
}

void BbCameraSession::handleVideoRecordingPaused()
{
    //TODO: implement once BB10 API supports pausing a video
}

void BbCameraSession::handleVideoRecordingResumed()
{
    if (m_videoStatus == QMediaRecorder::StartingStatus) {
        m_videoStatus = QMediaRecorder::RecordingStatus;
        emit videoStatusChanged(m_videoStatus);

        m_videoRecordingDuration.restart();
    }
}

void BbCameraSession::deviceOrientationChanged(int angle)
{
    if (m_handle != CAMERA_HANDLE_INVALID)
        camera_set_device_orientation(m_handle, angle);
}

void BbCameraSession::handleCameraPowerUp()
{
    stopViewFinder();
    startViewFinder();
}

void BbCameraSession::viewfinderFrameGrabbed(const QImage &image)
{
    QTransform transform;

    // subtract out the native rotation
    transform.rotate(m_nativeCameraOrientation);

    // subtract out the current device orientation
    if (m_device == cameraIdentifierRear())
        transform.rotate(360 - m_orientationHandler->viewfinderOrientation());
    else
        transform.rotate(m_orientationHandler->viewfinderOrientation());

    QImage frame = image.copy().transformed(transform);

    QMutexLocker locker(&m_surfaceMutex);
    if (m_surface) {
        if (frame.size() != m_surface->surfaceFormat().frameSize()) {
            m_surface->stop();
            m_surface->start(QVideoSurfaceFormat(frame.size(), QVideoFrame::Format_ARGB32));
        }

        QVideoFrame videoFrame(frame);

        m_surface->present(videoFrame);
    }
}

bool BbCameraSession::openCamera()
{
    if (m_handle != CAMERA_HANDLE_INVALID) // camera is already open
        return true;

    m_status = QCamera::LoadingStatus;
    emit statusChanged(m_status);

    camera_unit_t unit = CAMERA_UNIT_REAR;
    if (m_device == cameraIdentifierFront())
        unit = CAMERA_UNIT_FRONT;
    else if (m_device == cameraIdentifierRear())
        unit = CAMERA_UNIT_REAR;
    else if (m_device == cameraIdentifierDesktop())
        unit = CAMERA_UNIT_DESKTOP;

    camera_error_t result = camera_open(unit, CAMERA_MODE_RW, &m_handle);
    if (result != CAMERA_EOK) {
        m_handle = CAMERA_HANDLE_INVALID;
        m_status = QCamera::UnloadedStatus;
        emit statusChanged(m_status);

        qWarning() << "Unable to open camera:" << result;
        emit error(QCamera::CameraError, tr("Unable to open camera"));
        return false;
    }

    result = camera_get_native_orientation(m_handle, &m_nativeCameraOrientation);
    if (result != CAMERA_EOK) {
        qWarning() << "Unable to retrieve native camera orientation:" << result;
        emit error(QCamera::CameraError, tr("Unable to retrieve native camera orientation"));
        return false;
    }

    m_previewIsVideo = camera_has_feature(m_handle, CAMERA_FEATURE_PREVIEWISVIDEO);

    m_status = QCamera::LoadedStatus;
    emit statusChanged(m_status);

    emit cameraOpened();

    return true;
}

void BbCameraSession::closeCamera()
{
    if (m_handle == CAMERA_HANDLE_INVALID) // camera is closed already
        return;

    m_status = QCamera::UnloadingStatus;
    emit statusChanged(m_status);

    const camera_error_t result = camera_close(m_handle);
    if (result != CAMERA_EOK) {
        m_status = QCamera::LoadedStatus;
        emit statusChanged(m_status);

        qWarning() << "Unable to close camera:" << result;
        emit error(QCamera::CameraError, tr("Unable to close camera"));
        return;
    }

    m_handle = CAMERA_HANDLE_INVALID;

    m_status = QCamera::UnloadedStatus;
    emit statusChanged(m_status);
}

static void viewFinderStatusCallback(camera_handle_t handle, camera_devstatus_t status, uint16_t value, void *context)
{
    Q_UNUSED(handle)

    if (status == CAMERA_STATUS_FOCUS_CHANGE) {
        BbCameraSession *session = static_cast<BbCameraSession*>(context);
        QMetaObject::invokeMethod(session, "focusStatusChanged", Qt::QueuedConnection, Q_ARG(int, value));
        return;
    } else if (status == CAMERA_STATUS_POWERUP) {
        BbCameraSession *session = static_cast<BbCameraSession*>(context);
        QMetaObject::invokeMethod(session, "handleCameraPowerUp", Qt::QueuedConnection);
    }
}

bool BbCameraSession::startViewFinder()
{
    m_status = QCamera::StartingStatus;
    emit statusChanged(m_status);

    QSize viewfinderResolution;
    camera_error_t result = CAMERA_EOK;
    if (m_captureMode & QCamera::CaptureStillImage) {
        result = camera_start_photo_viewfinder(m_handle, 0, viewFinderStatusCallback, this);
        viewfinderResolution = currentViewfinderResolution(QCamera::CaptureStillImage);
    } else if (m_captureMode & QCamera::CaptureVideo) {
        result = camera_start_video_viewfinder(m_handle, 0, viewFinderStatusCallback, this);
        viewfinderResolution = currentViewfinderResolution(QCamera::CaptureVideo);
    }

    if (result != CAMERA_EOK) {
        qWarning() << "Unable to start viewfinder:" << result;
        return false;
    }

    const int angle = m_orientationHandler->viewfinderOrientation();

    const QSize rotatedSize = ((angle == 0 || angle == 180) ? viewfinderResolution
                                                            : viewfinderResolution.transposed());

    m_surfaceMutex.lock();
    if (m_surface) {
        const bool ok = m_surface->start(QVideoSurfaceFormat(rotatedSize, QVideoFrame::Format_ARGB32));
        if (!ok)
            qWarning() << "Unable to start camera viewfinder surface";
    }
    m_surfaceMutex.unlock();

    m_status = QCamera::ActiveStatus;
    emit statusChanged(m_status);

    return true;
}

void BbCameraSession::stopViewFinder()
{
    m_windowGrabber->stop();

    m_status = QCamera::StoppingStatus;
    emit statusChanged(m_status);

    m_surfaceMutex.lock();
    if (m_surface) {
        m_surface->stop();
    }
    m_surfaceMutex.unlock();

    camera_error_t result = CAMERA_EOK;
    if (m_captureMode & QCamera::CaptureStillImage)
        result = camera_stop_photo_viewfinder(m_handle);
    else if (m_captureMode & QCamera::CaptureVideo)
        result = camera_stop_video_viewfinder(m_handle);

    if (result != CAMERA_EOK) {
        qWarning() << "Unable to stop viewfinder:" << result;
        return;
    }

    m_status = QCamera::LoadedStatus;
    emit statusChanged(m_status);
}

void BbCameraSession::applyConfiguration()
{
    if (m_captureMode & QCamera::CaptureStillImage) {
        const QList<QSize> photoOutputResolutions = supportedResolutions(QCamera::CaptureStillImage);

        if (!m_imageEncoderSettings.resolution().isValid() || !photoOutputResolutions.contains(m_imageEncoderSettings.resolution()))
            m_imageEncoderSettings.setResolution(photoOutputResolutions.first());

        const QSize photoResolution = m_imageEncoderSettings.resolution();
        const qreal aspectRatio = static_cast<qreal>(photoResolution.width())/static_cast<qreal>(photoResolution.height());

        // apply viewfinder configuration
        QSize viewfinderResolution;
        QList<QSize> sizes = supportedViewfinderResolutions(QCamera::CaptureStillImage);
        std::reverse(sizes.begin(), sizes.end()); // use smallest possible resolution
        foreach (const QSize &size, sizes) {
            // search for viewfinder resolution with the same aspect ratio
            if (qFuzzyCompare(aspectRatio, (static_cast<qreal>(size.width())/static_cast<qreal>(size.height())))) {
                viewfinderResolution = size;
                break;
            }
        }

        Q_ASSERT(viewfinderResolution.isValid());

        const QByteArray windowId = QString().sprintf("qcamera_vf_%p", this).toLatin1();
        m_windowGrabber->setWindowId(windowId);

        const QByteArray windowGroupId = m_windowGrabber->windowGroupId();

        camera_error_t result = camera_set_photovf_property(m_handle,
                                                            CAMERA_IMGPROP_WIN_GROUPID, windowGroupId.data(),
                                                            CAMERA_IMGPROP_WIN_ID, windowId.data(),
                                                            CAMERA_IMGPROP_WIDTH, viewfinderResolution.width(),
                                                            CAMERA_IMGPROP_HEIGHT, viewfinderResolution.height(),
                                                            CAMERA_IMGPROP_FORMAT, CAMERA_FRAMETYPE_NV12,
                                                            CAMERA_IMGPROP_ROTATION, 360 - m_nativeCameraOrientation);

        if (result != CAMERA_EOK) {
            qWarning() << "Unable to apply photo viewfinder settings:" << result;
            return;
        }


        int jpegQuality = 100;
        switch (m_imageEncoderSettings.quality()) {
        case QMultimedia::VeryLowQuality:
            jpegQuality = 20;
            break;
        case QMultimedia::LowQuality:
            jpegQuality = 40;
            break;
        case QMultimedia::NormalQuality:
            jpegQuality = 60;
            break;
        case QMultimedia::HighQuality:
            jpegQuality = 80;
            break;
        case QMultimedia::VeryHighQuality:
            jpegQuality = 100;
            break;
        }

        // apply photo configuration
        result = camera_set_photo_property(m_handle,
                                           CAMERA_IMGPROP_WIDTH, photoResolution.width(),
                                           CAMERA_IMGPROP_HEIGHT, photoResolution.height(),
                                           CAMERA_IMGPROP_JPEGQFACTOR, jpegQuality,
                                           CAMERA_IMGPROP_ROTATION, 360 - m_nativeCameraOrientation);

        if (result != CAMERA_EOK) {
            qWarning() << "Unable to apply photo settings:" << result;
            return;
        }

    } else if (m_captureMode & QCamera::CaptureVideo) {
        applyVideoSettings();
    }

    startViewFinder();
}

static void videoRecordingStatusCallback(camera_handle_t handle, camera_devstatus_t status, uint16_t value, void *context)
{
    Q_UNUSED(handle)
    Q_UNUSED(value)

    if (status == CAMERA_STATUS_VIDEO_PAUSE) {
        BbCameraSession *session = static_cast<BbCameraSession*>(context);
        QMetaObject::invokeMethod(session, "handleVideoRecordingPaused", Qt::QueuedConnection);
    } else if (status == CAMERA_STATUS_VIDEO_RESUME) {
        BbCameraSession *session = static_cast<BbCameraSession*>(context);
        QMetaObject::invokeMethod(session, "handleVideoRecordingResumed", Qt::QueuedConnection);
    }
}

bool BbCameraSession::startVideoRecording()
{
    m_videoRecordingDuration.invalidate();

    m_videoStatus = QMediaRecorder::StartingStatus;
    emit videoStatusChanged(m_videoStatus);

    if (m_videoOutputLocation.isEmpty())
        m_videoOutputLocation = m_mediaStorageLocation.generateFileName(QLatin1String("VID_"), m_mediaStorageLocation.defaultDir(QCamera::CaptureVideo), QLatin1String("mp4"));

    emit actualLocationChanged(m_videoOutputLocation);

    const camera_error_t result = camera_start_video(m_handle, QFile::encodeName(m_videoOutputLocation), 0, videoRecordingStatusCallback, this);
    if (result != CAMERA_EOK) {
        m_videoStatus = QMediaRecorder::LoadedStatus;
        emit videoStatusChanged(m_videoStatus);

        emit videoError(QMediaRecorder::ResourceError, tr("Unable to start video recording"));
        return false;
    }

    return true;
}

void BbCameraSession::stopVideoRecording()
{
    m_videoStatus = QMediaRecorder::FinalizingStatus;
    emit videoStatusChanged(m_videoStatus);

    const camera_error_t result = camera_stop_video(m_handle);
    if (result != CAMERA_EOK) {
        emit videoError(QMediaRecorder::ResourceError, tr("Unable to stop video recording"));
    }

    m_videoStatus = QMediaRecorder::LoadedStatus;
    emit videoStatusChanged(m_videoStatus);

    m_videoRecordingDuration.invalidate();
}

bool BbCameraSession::isCaptureModeSupported(camera_handle_t handle, QCamera::CaptureModes mode) const
{
    if (mode & QCamera::CaptureStillImage)
        return camera_has_feature(handle, CAMERA_FEATURE_PHOTO);

    if (mode & QCamera::CaptureVideo)
        return camera_has_feature(handle, CAMERA_FEATURE_VIDEO);

    return false;
}

QList<QSize> BbCameraSession::supportedResolutions(QCamera::CaptureMode mode) const
{
    Q_ASSERT(m_handle != CAMERA_HANDLE_INVALID);

    QList<QSize> list;

    camera_error_t result = CAMERA_EOK;
    camera_res_t resolutions[20];
    unsigned int supported = 0;

    if (mode == QCamera::CaptureStillImage)
        result = camera_get_photo_output_resolutions(m_handle, CAMERA_FRAMETYPE_JPEG, 20, &supported, resolutions);
    else if (mode == QCamera::CaptureVideo)
        result = camera_get_video_output_resolutions(m_handle, 20, &supported, resolutions);

    if (result != CAMERA_EOK)
        return list;

    for (unsigned int i = 0; i < supported; ++i)
        list << QSize(resolutions[i].width, resolutions[i].height);

    return list;
}

QList<QSize> BbCameraSession::supportedViewfinderResolutions(QCamera::CaptureMode mode) const
{
    Q_ASSERT(m_handle != CAMERA_HANDLE_INVALID);

    QList<QSize> list;

    camera_error_t result = CAMERA_EOK;
    camera_res_t resolutions[20];
    unsigned int supported = 0;

    if (mode == QCamera::CaptureStillImage)
        result = camera_get_photo_vf_resolutions(m_handle, 20, &supported, resolutions);
    else if (mode == QCamera::CaptureVideo)
        result = camera_get_video_vf_resolutions(m_handle, 20, &supported, resolutions);

    if (result != CAMERA_EOK)
        return list;

    for (unsigned int i = 0; i < supported; ++i)
        list << QSize(resolutions[i].width, resolutions[i].height);

    return list;
}

QSize BbCameraSession::currentViewfinderResolution(QCamera::CaptureMode mode) const
{
    Q_ASSERT(m_handle != CAMERA_HANDLE_INVALID);

    camera_error_t result = CAMERA_EOK;
    int width = 0;
    int height = 0;

    if (mode == QCamera::CaptureStillImage)
        result = camera_get_photovf_property(m_handle, CAMERA_IMGPROP_WIDTH, &width,
                                                       CAMERA_IMGPROP_HEIGHT, &height);
    else if (mode == QCamera::CaptureVideo)
        result = camera_get_videovf_property(m_handle, CAMERA_IMGPROP_WIDTH, &width,
                                                       CAMERA_IMGPROP_HEIGHT, &height);

    if (result != CAMERA_EOK)
        return QSize();

    return QSize(width, height);
}

QT_END_NAMESPACE
