/* -LICENSE-START-
 ** Copyright (c) 2018 Blackmagic Design
 **
 ** Permission is hereby granted, free of charge, to any person or organization
 ** obtaining a copy of the software and accompanying documentation covered by
 ** this license (the "Software") to use, reproduce, display, distribute,
 ** execute, and transmit the Software, and to prepare derivative works of the
 ** Software, and to permit third-parties to whom the Software is furnished to
 ** do so, all subject to the following:
 **
 ** The copyright notices in the Software and this entire statement, including
 ** the above license grant, this restriction and the following disclaimer,
 ** must be included in all copies of the Software, in whole or in part, and
 ** all derivative works of the Software, unless such copies or derivative
 ** works are solely in the form of machine-executable object code generated by
 ** a source language processor.
 **
 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 ** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 ** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 ** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 ** DEALINGS IN THE SOFTWARE.
 ** -LICENSE-END-
 */

#import "DeckLinkCoreMediaReader.h"
#import "DeckLinkCoreMediaVideoFrame.h"
#import "CoreMediaPCMAudioBuffer.h"

// Audio configuration (must be compatible with DeckLink audio output)
// 2-channel 16-bit (DeckLink only supports 48kHz PCM)
static uint32_t kAudioChannelCount	= 2;
static uint32_t kAudioBitsPerSample	= bmdAudioSampleType16bitInteger;

DeckLinkCoreMediaReader::DeckLinkCoreMediaReader() :
m_avAsset(nullptr),
m_avAssetReader(nullptr),
m_readerState(kReaderIdle),
m_displayMode(),
m_frameWidth(0),
m_frameHeight(0),
m_streamTime(0),
m_streamTimescale(0)
{
}

DeckLinkCoreMediaReader::~DeckLinkCoreMediaReader()
{
	complete();
}

bool DeckLinkCoreMediaReader::init(const std::string& filePath, BMDTimeValue streamTime, BMDTimeScale streamTimeScale)
{
	std::lock_guard<std::mutex> lock(m_avAssetReaderMutex);
	
	bool			success		= false;
	NSError*		error		= nullptr;
	NSArray*		audioTracks	= nullptr;
	AVAssetTrack*	audioTrack	= nullptr;
	NSArray*		videoTracks	= nullptr;
	AVAssetTrack*	videoTrack	= nullptr;
	BMDTimeValue	startTime	= streamTime;

	NSDictionary *avAssetOptions = @{AVURLAssetPreferPreciseDurationAndTimingKey: @YES};

	m_avAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithFileSystemRepresentation:filePath.c_str() isDirectory:FALSE relativeToURL:nullptr] options:avAssetOptions];

	m_fileDuration = [m_avAsset duration];

	// Cancel and release the asset reader if reinitializing
	if (m_avAssetReader)
	{
		[m_avAssetReader cancelReading];
		m_audioStreamTime = 0;
	}

	// Initialize the AVAssetReader
	m_avAssetReader = [[AVAssetReader alloc] initWithAsset:m_avAsset error:&error];
	if (m_avAssetReader == nullptr)
		goto bail;

	// Handle audio tracks
	audioTracks = [m_avAsset tracksWithMediaType:AVMediaTypeAudio];

	// For simplicity if multiple audio tracks are available, use the first one
	if ([audioTracks count] > 0)
		audioTrack = audioTracks[0];

	if (audioTrack != nil)
	{
		NSDictionary *outputAudioSettings = @{AVFormatIDKey: @(kAudioFormatLinearPCM),
											  AVSampleRateKey: @(bmdAudioSampleRate48kHz),
											  AVLinearPCMBitDepthKey: @(kAudioBitsPerSample),
											  AVNumberOfChannelsKey: @(kAudioChannelCount),
											  AVLinearPCMIsBigEndianKey: @NO,
											  AVLinearPCMIsFloatKey: @NO,
											  AVLinearPCMIsNonInterleaved: @NO};

		AVAssetReaderOutput *audioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:outputAudioSettings];
		[m_avAssetReader addOutput:audioOutput];
	}


	videoTracks = [m_avAsset tracksWithMediaType:AVMediaTypeVideo];

	if ([videoTracks count] > 0)
		videoTrack = videoTracks[0];

	if (videoTrack)
	{
		// Read frames into 8-bit BGRA buffers
		NSDictionary *outputVideoSettings = @{(id) kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
											  (id) kCVPixelBufferIOSurfacePropertiesKey: [NSDictionary dictionary]};

		AVAssetReaderOutput* videoOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:videoTrack outputSettings:outputVideoSettings];
		[m_avAssetReader addOutput:videoOutput];
	}

	// Read from the requested stream time until the end of the file
	m_avAssetReader.timeRange = CMTimeRangeMake(CMTimeMake(startTime, (uint32_t)streamTimeScale), kCMTimePositiveInfinity);

	if (![m_avAssetReader startReading])
		goto bail;

	m_streamTime	= startTime;
	m_filePath		= filePath;

	success = true;

bail:
	m_init = success;
	return m_init;
}

uint32_t DeckLinkCoreMediaReader::audioBitsPerSample() const
{
	return kAudioBitsPerSample;
}

uint32_t DeckLinkCoreMediaReader::audioChannelCount() const
{
	return kAudioChannelCount;
}

bool DeckLinkCoreMediaReader::duration(BMDTimeValue& duration, BMDTimeScale timeScale) const
{
	if (!m_init)
		return false;

	duration = (m_fileDuration.value * timeScale) / m_fileDuration.timescale;
	return true;
}

bool DeckLinkCoreMediaReader::reset(BMDTimeValue streamTime, BMDTimeScale streamTimeScale)
{
	if (!m_init)
		return false;

	return init(m_filePath, streamTime, streamTimeScale);
}

bool DeckLinkCoreMediaReader::previewFrame(com_ptr<IDeckLinkVideoFrame>& previewFrame, float* frameRate)
{
	AVAssetTrack*	videoTrack	= nullptr;
	NSArray*		videoTracks	= [m_avAsset tracksWithMediaType:AVMediaTypeVideo];

	if ([videoTracks count] > 0)
		videoTrack = videoTracks[0];
	else
		return false;

	if (frameRate)
		*frameRate = videoTrack.nominalFrameRate;

	if (!readVideo(previewFrame))
		return false;

	return true;
}

bool DeckLinkCoreMediaReader::readVideo(com_ptr<IDeckLinkVideoFrame>& videoFrame)
{
	std::lock_guard<std::mutex> lock(m_avAssetReaderMutex);
	
	bool success = false;
	CMSampleBufferRef sampleBuffer = nullptr;

	NSArray *outputs = [m_avAssetReader outputs];

	for (AVAssetReaderOutput* output in outputs)
	{
		// Process only video buffers
		if (![[output mediaType] isEqualToString:AVMediaTypeVideo])
			continue;

		sampleBuffer = [output copyNextSampleBuffer];
		if (!sampleBuffer)
			continue;

		com_ptr<DeckLinkCoreMediaVideoFrame> cmVideoFrame = make_com_ptr<DeckLinkCoreMediaVideoFrame>();

		// Initialize DeckLink video frame for output
		if (!cmVideoFrame->setVideoBuffer(sampleBuffer))
			goto bail;

		m_streamTime = (BMDTimeValue)CMSampleBufferGetPresentationTimeStamp(sampleBuffer).value;

		// Set the output frame
		cmVideoFrame->QueryInterface(IID_IDeckLinkVideoFrame, (void**)&videoFrame);
		
		// Release the reference added by QueryInterface
		cmVideoFrame->Release();
		
		// Only process the first video output
		break;
	}
	success = true;

bail:
	if (sampleBuffer)
		CFRelease(sampleBuffer);

	return success;
}

bool DeckLinkCoreMediaReader::readAudio(std::unique_ptr<PCMAudioBuffer>& pcmAudioBuffer)
{
	std::lock_guard<std::mutex> lock(m_avAssetReaderMutex);
	
	bool success = false;
	CMSampleBufferRef sampleBuffer = nullptr;

	NSArray *outputs = [m_avAssetReader outputs];
	for (AVAssetReaderOutput* output in outputs)
	{
		// Process only audio buffers
		if (![[output mediaType] isEqualToString:AVMediaTypeAudio])
			continue;

		sampleBuffer = [output copyNextSampleBuffer];

		if (!sampleBuffer)
			continue;

		// Create a new audio buffer from the sample buffer
		pcmAudioBuffer.reset(new CoreMediaPCMAudioBuffer(sampleBuffer));
		success = true;

		// Only process the first audio sample buffer
		break;
	}

	if (sampleBuffer)
		CFRelease(sampleBuffer);

	return success;
}

bool DeckLinkCoreMediaReader::complete()
{
	if (m_avAssetReader)
	{
		std::lock_guard<std::mutex> lock(m_avAssetReaderMutex);
		[m_avAssetReader cancelReading];
		m_avAssetReader = nullptr;
	}

	m_avAsset = nullptr;

	m_audioStreamTime = 0;

	return true;
}

std::string DeckLinkCoreMediaReader::filePath() const
{
	return m_filePath;
}
