/****************************************************************************
**
** 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 "qandroidcapturesession.h"

#include "androidcamera.h"
#include "qandroidcamerasession.h"
#include "androidmultimediautils.h"
#include "qandroidmultimediautils.h"
#include "qandroidvideooutput.h"
#include "qandroidglobal.h"

#include <algorithm>

QT_BEGIN_NAMESPACE

QAndroidCaptureSession::QAndroidCaptureSession(QAndroidCameraSession *cameraSession)
    : QObject()
    , m_mediaRecorder(0)
    , m_cameraSession(cameraSession)
    , m_audioSource(AndroidMediaRecorder::DefaultAudioSource)
    , m_duration(0)
    , m_state(QMediaRecorder::StoppedState)
    , m_status(QMediaRecorder::UnloadedStatus)
    , m_containerFormatDirty(true)
    , m_videoSettingsDirty(true)
    , m_audioSettingsDirty(true)
    , m_outputFormat(AndroidMediaRecorder::DefaultOutputFormat)
    , m_audioEncoder(AndroidMediaRecorder::DefaultAudioEncoder)
    , m_videoEncoder(AndroidMediaRecorder::DefaultVideoEncoder)
{
    m_mediaStorageLocation.addStorageLocation(
                QMediaStorageLocation::Movies,
                AndroidMultimediaUtils::getDefaultMediaDirectory(AndroidMultimediaUtils::DCIM));

    m_mediaStorageLocation.addStorageLocation(
                QMediaStorageLocation::Sounds,
                AndroidMultimediaUtils::getDefaultMediaDirectory(AndroidMultimediaUtils::Sounds));

    if (cameraSession) {
        connect(cameraSession, SIGNAL(opened()), this, SLOT(onCameraOpened()));
        connect(cameraSession, &QAndroidCameraSession::statusChanged, this,
            [this](QCamera::Status status) {
                if (status == QCamera::UnavailableStatus) {
                    setState(QMediaRecorder::StoppedState);
                    setStatus(QMediaRecorder::UnavailableStatus);
                    return;
                }

                // Stop recording when stopping the camera.
                if (status == QCamera::StoppingStatus) {
                    setState(QMediaRecorder::StoppedState);
                    setStatus(QMediaRecorder::UnloadedStatus);
                    return;
                }

                if (status == QCamera::LoadingStatus)
                    setStatus(QMediaRecorder::LoadingStatus);
            });
        connect(cameraSession, &QAndroidCameraSession::captureModeChanged, this,
            [this](QCamera::CaptureModes mode) {
                if (!mode.testFlag(QCamera::CaptureVideo)) {
                    setState(QMediaRecorder::StoppedState);
                    setStatus(QMediaRecorder::UnloadedStatus);
                }
            });
        connect(cameraSession, &QAndroidCameraSession::readyForCaptureChanged, this,
            [this](bool ready) {
                if (ready)
                    setStatus(QMediaRecorder::LoadedStatus);
            });
    } else {
        // Audio-only recording.
        setStatus(QMediaRecorder::LoadedStatus);
    }

    m_notifyTimer.setInterval(1000);
    connect(&m_notifyTimer, SIGNAL(timeout()), this, SLOT(updateDuration()));
}

QAndroidCaptureSession::~QAndroidCaptureSession()
{
    stop();
    delete m_mediaRecorder;
}

void QAndroidCaptureSession::setAudioInput(const QString &input)
{
    if (m_audioInput == input)
        return;

    m_audioInput = input;

    if (m_audioInput == QLatin1String("default"))
        m_audioSource = AndroidMediaRecorder::DefaultAudioSource;
    else if (m_audioInput == QLatin1String("mic"))
        m_audioSource = AndroidMediaRecorder::Mic;
    else if (m_audioInput == QLatin1String("voice_uplink"))
        m_audioSource = AndroidMediaRecorder::VoiceUplink;
    else if (m_audioInput == QLatin1String("voice_downlink"))
        m_audioSource = AndroidMediaRecorder::VoiceDownlink;
    else if (m_audioInput == QLatin1String("voice_call"))
        m_audioSource = AndroidMediaRecorder::VoiceCall;
    else if (m_audioInput == QLatin1String("voice_recognition"))
        m_audioSource = AndroidMediaRecorder::VoiceRecognition;
    else
        m_audioSource = AndroidMediaRecorder::DefaultAudioSource;

    emit audioInputChanged(m_audioInput);
}

QUrl QAndroidCaptureSession::outputLocation() const
{
    return m_actualOutputLocation;
}

bool QAndroidCaptureSession::setOutputLocation(const QUrl &location)
{
    if (m_requestedOutputLocation == location)
        return false;

    m_actualOutputLocation = QUrl();
    m_requestedOutputLocation = location;

    if (m_requestedOutputLocation.isEmpty())
        return true;

    if (m_requestedOutputLocation.isValid()
            && (m_requestedOutputLocation.isLocalFile() || m_requestedOutputLocation.isRelative())) {
        return true;
    }

    m_requestedOutputLocation = QUrl();
    return false;
}

QMediaRecorder::State QAndroidCaptureSession::state() const
{
    return m_state;
}

void QAndroidCaptureSession::setState(QMediaRecorder::State state)
{
    if (m_state == state)
        return;

    switch (state) {
    case QMediaRecorder::StoppedState:
        stop();
        break;
    case QMediaRecorder::RecordingState:
        start();
        break;
    case QMediaRecorder::PausedState:
        pause();
        break;
    }
}

void QAndroidCaptureSession::start()
{
    if (m_state == QMediaRecorder::RecordingState
        || (m_status != QMediaRecorder::LoadedStatus && m_status != QMediaRecorder::PausedStatus))
        return;

    setStatus(QMediaRecorder::StartingStatus);

    if (m_state == QMediaRecorder::PausedState) {
        if (!m_mediaRecorder || !m_mediaRecorder->resume()) {
            emit error(QMediaRecorder::FormatError, QLatin1String("Unable to resume the media recorder."));
            if (m_cameraSession)
                restartViewfinder();
        } else {
            updateStartState();
        }
        return;
    }

    if (m_mediaRecorder) {
        m_mediaRecorder->release();
        delete m_mediaRecorder;
    }

    const bool granted = m_cameraSession
                       ? m_cameraSession->requestRecordingPermission()
                       : qt_androidRequestRecordingPermission();
    if (!granted) {
        setStatus(QMediaRecorder::UnavailableStatus);
        Q_EMIT error(QMediaRecorder::ResourceError, QLatin1String("Permission denied."));
        return;
    }

    m_mediaRecorder = new AndroidMediaRecorder;
    connect(m_mediaRecorder, SIGNAL(error(int,int)), this, SLOT(onError(int,int)));
    connect(m_mediaRecorder, SIGNAL(info(int,int)), this, SLOT(onInfo(int,int)));

    // Set audio/video sources
    if (m_cameraSession) {
        updateViewfinder();
        m_cameraSession->camera()->unlock();
        m_mediaRecorder->setCamera(m_cameraSession->camera());
        m_mediaRecorder->setAudioSource(AndroidMediaRecorder::Camcorder);
        m_mediaRecorder->setVideoSource(AndroidMediaRecorder::Camera);
    } else {
        m_mediaRecorder->setAudioSource(m_audioSource);
    }

    // Set output format
    m_mediaRecorder->setOutputFormat(m_outputFormat);

    // Set audio encoder settings
    m_mediaRecorder->setAudioChannels(m_audioSettings.channelCount());
    m_mediaRecorder->setAudioEncodingBitRate(m_audioSettings.bitRate());
    m_mediaRecorder->setAudioSamplingRate(m_audioSettings.sampleRate());
    m_mediaRecorder->setAudioEncoder(m_audioEncoder);

    // Set video encoder settings
    if (m_cameraSession) {
        m_mediaRecorder->setVideoSize(m_videoSettings.resolution());
        m_mediaRecorder->setVideoFrameRate(qRound(m_videoSettings.frameRate()));
        m_mediaRecorder->setVideoEncodingBitRate(m_videoSettings.bitRate());
        m_mediaRecorder->setVideoEncoder(m_videoEncoder);

        m_mediaRecorder->setOrientationHint(m_cameraSession->currentCameraRotation());
    }

    // Set output file
    QString filePath = m_mediaStorageLocation.generateFileName(
                m_requestedOutputLocation.isLocalFile() ? m_requestedOutputLocation.toLocalFile()
                                                        : m_requestedOutputLocation.toString(),
                m_cameraSession ? QMediaStorageLocation::Movies
                                : QMediaStorageLocation::Sounds,
                m_cameraSession ? QLatin1String("VID_")
                                : QLatin1String("REC_"),
                m_containerFormat);

    m_usedOutputLocation = QUrl::fromLocalFile(filePath);
    m_mediaRecorder->setOutputFile(filePath);

    // Even though the Android doc explicitly says that calling MediaRecorder.setPreviewDisplay()
    // is not necessary when the Camera already has a Surface, it doesn't actually work on some
    // devices. For example on the Samsung Galaxy Tab 2, the camera server dies after prepare()
    // and start() if MediaRecorder.setPreviewDispaly() is not called.
    if (m_cameraSession) {
        // When using a SurfaceTexture, we need to pass a new one to the MediaRecorder, not the same
        // one that is set on the Camera or it will crash, hence the reset().
        m_cameraSession->videoOutput()->reset();
        if (m_cameraSession->videoOutput()->surfaceTexture())
            m_mediaRecorder->setSurfaceTexture(m_cameraSession->videoOutput()->surfaceTexture());
        else if (m_cameraSession->videoOutput()->surfaceHolder())
            m_mediaRecorder->setSurfaceHolder(m_cameraSession->videoOutput()->surfaceHolder());
    }

    if (!m_mediaRecorder->prepare()) {
        emit error(QMediaRecorder::FormatError, QLatin1String("Unable to prepare the media recorder."));
        if (m_cameraSession)
            restartViewfinder();
        return;
    }

    if (!m_mediaRecorder->start()) {
        emit error(QMediaRecorder::FormatError, QLatin1String("Unable to start the media recorder."));
        if (m_cameraSession)
            restartViewfinder();
        return;
    }
    updateStartState();
}

void QAndroidCaptureSession::updateStartState()
{
    m_elapsedTime.start();
    m_notifyTimer.start();
    updateDuration();

    if (m_cameraSession) {
        m_cameraSession->setReadyForCapture(false);

        // Preview frame callback is cleared when setting up the camera with the media recorder.
        // We need to reset it.
        m_cameraSession->camera()->setupPreviewFrameCallback();
    }

    QMediaRecorder::State oldState = m_state;
    QMediaRecorder::Status oldStatus = m_status;

    m_state = QMediaRecorder::RecordingState;
    m_status = QMediaRecorder::RecordingStatus;

    m_actualOutputLocation = m_usedOutputLocation;
    emit actualLocationChanged(m_actualOutputLocation);

    if (m_state != oldState)
        emit stateChanged(m_state);

    if (m_status != oldStatus)
        emit statusChanged(m_status);
}

void QAndroidCaptureSession::stop(bool error)
{
    if (m_state == QMediaRecorder::StoppedState || m_mediaRecorder == 0)
        return;

    setStatus(QMediaRecorder::FinalizingStatus);

    m_mediaRecorder->stop();
    m_notifyTimer.stop();
    updateDuration();
    m_previousElapsedTime = 0;
    m_elapsedTime.invalidate();
    m_mediaRecorder->release();
    delete m_mediaRecorder;
    m_mediaRecorder = 0;

    if (m_cameraSession && m_cameraSession->status() == QCamera::ActiveStatus) {
        // Viewport needs to be restarted after recording
        restartViewfinder();
    }

    if (!error) {
        // if the media is saved into the standard media location, register it
        // with the Android media scanner so it appears immediately in apps
        // such as the gallery.
        QString mediaPath = m_usedOutputLocation.toLocalFile();
        QString standardLoc = m_cameraSession ? AndroidMultimediaUtils::getDefaultMediaDirectory(AndroidMultimediaUtils::DCIM)
                                              : AndroidMultimediaUtils::getDefaultMediaDirectory(AndroidMultimediaUtils::Sounds);
        if (mediaPath.startsWith(standardLoc))
            AndroidMultimediaUtils::registerMediaFile(mediaPath);

        m_actualOutputLocation = m_usedOutputLocation;
        emit actualLocationChanged(m_actualOutputLocation);
    }

    m_state = QMediaRecorder::StoppedState;
    emit stateChanged(m_state);
    if (!m_cameraSession)
        setStatus(QMediaRecorder::LoadedStatus);
}

void QAndroidCaptureSession::pause()
{
    if (m_state == QMediaRecorder::PausedState || m_mediaRecorder == 0)
        return;

    setStatus(QMediaRecorder::PausedStatus);

    m_mediaRecorder->pause();
    m_notifyTimer.stop();
    updateDuration();
    m_previousElapsedTime = m_duration;
    m_elapsedTime.invalidate();

    m_state = QMediaRecorder::PausedState;
    emit stateChanged(m_state);
}

void QAndroidCaptureSession::setStatus(QMediaRecorder::Status status)
{
    if (m_status == status)
        return;

    m_status = status;
    emit statusChanged(m_status);
}

QMediaRecorder::Status QAndroidCaptureSession::status() const
{
    return m_status;
}

qint64 QAndroidCaptureSession::duration() const
{
    return m_duration;
}

void QAndroidCaptureSession::setContainerFormat(const QString &format)
{
    if (m_containerFormat == format)
        return;

    m_containerFormat = format;
    m_containerFormatDirty = true;
}

void QAndroidCaptureSession::setAudioSettings(const QAudioEncoderSettings &settings)
{
    if (m_audioSettings == settings)
        return;

    m_audioSettings = settings;
    m_audioSettingsDirty = true;
}

void QAndroidCaptureSession::setVideoSettings(const QVideoEncoderSettings &settings)
{
    if (!m_cameraSession || m_videoSettings == settings)
        return;

    m_videoSettings = settings;
    m_videoSettingsDirty = true;
}

void QAndroidCaptureSession::applySettings()
{
    // container settings
    if (m_containerFormatDirty) {
        if (m_containerFormat.isEmpty()) {
            m_containerFormat = m_defaultSettings.outputFileExtension;
            m_outputFormat = m_defaultSettings.outputFormat;
        } else if (m_containerFormat == QLatin1String("3gp")) {
            m_outputFormat = AndroidMediaRecorder::THREE_GPP;
        } else if (!m_cameraSession && m_containerFormat == QLatin1String("amr")) {
            m_outputFormat = AndroidMediaRecorder::AMR_NB_Format;
        } else if (!m_cameraSession && m_containerFormat == QLatin1String("awb")) {
            m_outputFormat = AndroidMediaRecorder::AMR_WB_Format;
        } else {
            m_containerFormat = QStringLiteral("mp4");
            m_outputFormat = AndroidMediaRecorder::MPEG_4;
        }

        m_containerFormatDirty = false;
    }

    // audio settings
    if (m_audioSettingsDirty) {
        if (m_audioSettings.channelCount() <= 0)
            m_audioSettings.setChannelCount(m_defaultSettings.audioChannels);
        if (m_audioSettings.bitRate() <= 0)
            m_audioSettings.setBitRate(m_defaultSettings.audioBitRate);
        if (m_audioSettings.sampleRate() <= 0)
            m_audioSettings.setSampleRate(m_defaultSettings.audioSampleRate);

        if (m_audioSettings.codec().isEmpty())
            m_audioEncoder = m_defaultSettings.audioEncoder;
        else if (m_audioSettings.codec() == QLatin1String("aac"))
            m_audioEncoder = AndroidMediaRecorder::AAC;
        else if (m_audioSettings.codec() == QLatin1String("amr-nb"))
            m_audioEncoder = AndroidMediaRecorder::AMR_NB_Encoder;
        else if (m_audioSettings.codec() == QLatin1String("amr-wb"))
            m_audioEncoder = AndroidMediaRecorder::AMR_WB_Encoder;
        else
            m_audioEncoder = m_defaultSettings.audioEncoder;

        m_audioSettingsDirty = false;
    }

    // video settings
    if (m_cameraSession && m_cameraSession->camera() && m_videoSettingsDirty) {
        if (m_videoSettings.resolution().isEmpty()) {
            m_videoSettings.setResolution(m_defaultSettings.videoResolution);
        } else if (!m_supportedResolutions.contains(m_videoSettings.resolution())) {
            // if the requested resolution is not supported, find the closest one
            QSize reqSize = m_videoSettings.resolution();
            int reqPixelCount = reqSize.width() * reqSize.height();
            QList<int> supportedPixelCounts;
            for (int i = 0; i < m_supportedResolutions.size(); ++i) {
                const QSize &s = m_supportedResolutions.at(i);
                supportedPixelCounts.append(s.width() * s.height());
            }
            int closestIndex = qt_findClosestValue(supportedPixelCounts, reqPixelCount);
            m_videoSettings.setResolution(m_supportedResolutions.at(closestIndex));
        }

        if (m_videoSettings.frameRate() <= 0)
            m_videoSettings.setFrameRate(m_defaultSettings.videoFrameRate);
        if (m_videoSettings.bitRate() <= 0)
            m_videoSettings.setBitRate(m_defaultSettings.videoBitRate);

        if (m_videoSettings.codec().isEmpty())
            m_videoEncoder = m_defaultSettings.videoEncoder;
        else if (m_videoSettings.codec() == QLatin1String("h263"))
            m_videoEncoder = AndroidMediaRecorder::H263;
        else if (m_videoSettings.codec() == QLatin1String("h264"))
            m_videoEncoder = AndroidMediaRecorder::H264;
        else if (m_videoSettings.codec() == QLatin1String("mpeg4_sp"))
            m_videoEncoder = AndroidMediaRecorder::MPEG_4_SP;
        else
            m_videoEncoder = m_defaultSettings.videoEncoder;

        m_videoSettingsDirty = false;
    }
}

void QAndroidCaptureSession::updateViewfinder()
{
    m_cameraSession->camera()->stopPreviewSynchronous();
    m_cameraSession->applyViewfinderSettings(m_videoSettings.resolution(), false);
}

void QAndroidCaptureSession::restartViewfinder()
{
    if (!m_cameraSession)
        return;

    m_cameraSession->camera()->reconnect();

    // This is not necessary on most devices, but it crashes on some if we don't stop the
    // preview and reset the preview display on the camera when recording is over.
    m_cameraSession->camera()->stopPreviewSynchronous();
    m_cameraSession->videoOutput()->reset();
    if (m_cameraSession->videoOutput()->surfaceTexture())
        m_cameraSession->camera()->setPreviewTexture(m_cameraSession->videoOutput()->surfaceTexture());
    else if (m_cameraSession->videoOutput()->surfaceHolder())
        m_cameraSession->camera()->setPreviewDisplay(m_cameraSession->videoOutput()->surfaceHolder());

    m_cameraSession->camera()->startPreview();
    m_cameraSession->setReadyForCapture(true);
}

void QAndroidCaptureSession::updateDuration()
{
    if (m_elapsedTime.isValid())
        m_duration = m_elapsedTime.elapsed() + m_previousElapsedTime;

    emit durationChanged(m_duration);
}

void QAndroidCaptureSession::onCameraOpened()
{
    m_supportedResolutions.clear();
    m_supportedFramerates.clear();

    // get supported resolutions from predefined profiles
    for (int i = 0; i < 8; ++i) {
        CaptureProfile profile = getProfile(i);
        if (!profile.isNull) {
            if (i == AndroidCamcorderProfile::QUALITY_HIGH)
                m_defaultSettings = profile;

            if (!m_supportedResolutions.contains(profile.videoResolution))
                m_supportedResolutions.append(profile.videoResolution);
            if (!m_supportedFramerates.contains(profile.videoFrameRate))
                m_supportedFramerates.append(profile.videoFrameRate);
        }
    }

    std::sort(m_supportedResolutions.begin(), m_supportedResolutions.end(), qt_sizeLessThan);
    std::sort(m_supportedFramerates.begin(), m_supportedFramerates.end());

    applySettings();
}

QAndroidCaptureSession::CaptureProfile QAndroidCaptureSession::getProfile(int id)
{
    CaptureProfile profile;
    const bool hasProfile = AndroidCamcorderProfile::hasProfile(m_cameraSession->camera()->cameraId(),
                                                                AndroidCamcorderProfile::Quality(id));

    if (hasProfile) {
        AndroidCamcorderProfile camProfile = AndroidCamcorderProfile::get(m_cameraSession->camera()->cameraId(),
                                                                          AndroidCamcorderProfile::Quality(id));

        profile.outputFormat = AndroidMediaRecorder::OutputFormat(camProfile.getValue(AndroidCamcorderProfile::fileFormat));
        profile.audioEncoder = AndroidMediaRecorder::AudioEncoder(camProfile.getValue(AndroidCamcorderProfile::audioCodec));
        profile.audioBitRate = camProfile.getValue(AndroidCamcorderProfile::audioBitRate);
        profile.audioChannels = camProfile.getValue(AndroidCamcorderProfile::audioChannels);
        profile.audioSampleRate = camProfile.getValue(AndroidCamcorderProfile::audioSampleRate);
        profile.videoEncoder = AndroidMediaRecorder::VideoEncoder(camProfile.getValue(AndroidCamcorderProfile::videoCodec));
        profile.videoBitRate = camProfile.getValue(AndroidCamcorderProfile::videoBitRate);
        profile.videoFrameRate = camProfile.getValue(AndroidCamcorderProfile::videoFrameRate);
        profile.videoResolution = QSize(camProfile.getValue(AndroidCamcorderProfile::videoFrameWidth),
                                        camProfile.getValue(AndroidCamcorderProfile::videoFrameHeight));

        if (profile.outputFormat == AndroidMediaRecorder::MPEG_4)
            profile.outputFileExtension = QStringLiteral("mp4");
        else if (profile.outputFormat == AndroidMediaRecorder::THREE_GPP)
            profile.outputFileExtension = QStringLiteral("3gp");
        else if (profile.outputFormat == AndroidMediaRecorder::AMR_NB_Format)
            profile.outputFileExtension = QStringLiteral("amr");
        else if (profile.outputFormat == AndroidMediaRecorder::AMR_WB_Format)
            profile.outputFileExtension = QStringLiteral("awb");

        profile.isNull = false;
    }

    return profile;
}

void QAndroidCaptureSession::onError(int what, int extra)
{
    Q_UNUSED(what)
    Q_UNUSED(extra)
    stop(true);
    emit error(QMediaRecorder::ResourceError, QLatin1String("Unknown error."));
}

void QAndroidCaptureSession::onInfo(int what, int extra)
{
    Q_UNUSED(extra)
    if (what == 800) {
        // MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
        setState(QMediaRecorder::StoppedState);
        emit error(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum duration reached."));
    } else if (what == 801) {
        // MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
        setState(QMediaRecorder::StoppedState);
        emit error(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum file size reached."));
    }
}

QT_END_NAMESPACE
