/* -LICENSE-START-
** Copyright (c) 2011 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 "OpenGLPreview.h"

#import <sys/time.h>

// Buffer some frames before starting playback to allow for delays in the
// pipeline.  These occur as a result of the hugely variable compressed frame
// size, which affects both data retrieval latency from the device and decoder
// decompression time.
const int kFrameBufferMicros = 500000;

static uint64_t GetMicros()
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec * 1000000ULL + tv.tv_usec;
}

@implementation OpenGLPreview

static CVReturn DisplayLinkCallbackFunction(CVDisplayLinkRef	displayLink,
											const CVTimeStamp*	inNow,
											const CVTimeStamp*	inOutputTime, 
											CVOptionFlags		flagsIn,
											CVOptionFlags*		flagsOut,
											void*				arg)
{
	OpenGLPreview* realSelf = (OpenGLPreview*)arg;

	return [realSelf displayFrame:inOutputTime];
}

- (void)awakeFromNib
{
	m_lock = [[NSRecursiveLock alloc] init];
	m_frameMapLock = [[NSRecursiveLock alloc] init];
	m_hostTimeScale = ::CVGetHostClockFrequency();
	m_firstFrameArrivalTime = 0;
}

- (void)clearFrameQueue
{
	frame_map_t::iterator it;
	
	[m_frameMapLock lock];
	{
		for (it = m_frameMap.begin(); it != m_frameMap.end(); ++it)
			::CVPixelBufferRelease(it->second);

		m_frameMap.clear();
	}
	[m_frameMapLock unlock];
}

- (void)dealloc
{
	if (m_displayLink != NULL)
	{
		::CVDisplayLinkStop(m_displayLink);
		::CVDisplayLinkRelease(m_displayLink);
		m_displayLink = NULL;
	}
	
	[m_lock release];
	m_lock = nil;

	[m_frameMapLock release];
	m_frameMapLock = nil;
	
	[self clearFrameQueue];
	
	[super dealloc];
}

- (void)prepareOpenGL
{
	[super prepareOpenGL];
	
	GLint swapInterval = 1;
	[[self openGLContext] setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval];
	
	CVReturn			error;
	CGDirectDisplayID	displayID = ::CGMainDisplayID();
	
	error = ::CVDisplayLinkCreateWithCGDisplay(displayID, &m_displayLink);
	
	if (error == kCVReturnSuccess)
	{
		// Set the current display
		error = ::CVDisplayLinkSetCurrentCGDisplay(m_displayLink, kCGDirectMainDisplay);
		if (error)
			NSLog(@"Failed to set current CG display\n");
		
		// Set the output callback
		error = ::CVDisplayLinkSetOutputCallback(m_displayLink, &DisplayLinkCallbackFunction, self);
		if (error)
			NSLog(@"Failed to set output callback\n");
	}
	else
	{
		NSLog(@"Failed to create display link: %i\n", error);
		m_displayLink = NULL;
	}
}

- (void)reshape
{
	m_needsReshape = YES;
}

- (void)enqueueVideoFrame:(CVPixelBufferRef)pixBuf fromNAL:(IBMDStreamingH264NALPacket*)nal
{
	uint64_t frameTime;
	nal->GetDisplayTime(m_hostTimeScale, &frameTime);
		
	// Hold a reference to the pixel buffer
	::CVPixelBufferRetain(pixBuf);

	[m_frameMapLock lock];
	m_frameMap[frameTime] = pixBuf;
	[m_frameMapLock unlock];

	if (m_firstFrameArrivalTime == 0)
		m_firstFrameArrivalTime = GetMicros();
}

- (void)putPixbufInTexture:(CVPixelBufferRef)pixBuf
{
	void*	address;
	long	rowBytes;
	int		frameWidth, frameHeight;
	bool	newTexture = false;
	int		type;

	[[self openGLContext] makeCurrentContext];
	
	if (::CVPixelBufferLockBaseAddress(pixBuf, 0))
	{
		NSLog(@"CVPixelBufferLockBaseAddress failed\n");
		return;
	}
	
	address     = ::CVPixelBufferGetBaseAddress(pixBuf);
	rowBytes    = ::CVPixelBufferGetBytesPerRow(pixBuf);
	frameWidth  = ::CVPixelBufferGetWidth(pixBuf);
	frameHeight = ::CVPixelBufferGetHeight(pixBuf);
	
	// If this frame's dimensions differ to those of the existing texture,
	// create a new texture object.
	if (((frameWidth != m_textureWidth) || (frameHeight != m_textureHeight)) && (m_textureId != 0))
	{
		glDeleteTextures(1, &m_textureId);
		m_textureId = 0;
	}
	
	if (! m_textureId)
	{
		glGenTextures(1, &m_textureId);
		
		m_textureWidth = frameWidth;
		m_textureHeight = frameHeight;
		newTexture = true;
	}
	
	glBindTexture(GL_TEXTURE_RECTANGLE_EXT, m_textureId);
	glPixelStorei(GL_UNPACK_ROW_LENGTH, rowBytes / 2);
	
	glDisable(GL_FRAGMENT_PROGRAM_ARB);
	
#if TARGET_RT_BIG_ENDIAN
	type = GL_UNSIGNED_SHORT_8_8_REV_APPLE;
#else
	type = GL_UNSIGNED_SHORT_8_8_APPLE;
#endif
	
	if (newTexture)
	{
		// Use glTexImage2D() since this is a new texture.
		glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, 0, GL_RGB, m_textureWidth, m_textureHeight, 0, GL_YCBCR_422_APPLE, type, address);
	}
	else
	{
		// Use glTexSubImageID() since this is an existing texture.
		glTexSubImage2D(GL_TEXTURE_RECTANGLE_EXT, 0, 0, 0, m_textureWidth, m_textureHeight, GL_YCBCR_422_APPLE, type, address);
	}
	if (::CVPixelBufferUnlockBaseAddress(pixBuf, 0))
	{
		NSLog(@"CVPixelBufferUnlockBaseAddress failed\n");
	}
	
	return;
}

- (BOOL)getFrameForTime:(const CVTimeStamp*)timeStamp
{
	CVPixelBufferRef pixelBuf = NULL;

	[m_frameMapLock lock];

	if (!m_frameMap.empty())
	{
		if (m_displayLinkHostStartTime == 0 && GetMicros() > m_firstFrameArrivalTime + kFrameBufferMicros)
		{
			pixelBuf = m_frameMap.begin()->second;
			m_displayLinkHostStartTime = timeStamp->hostTime;
			m_displayLinkStreamStartTime = m_frameMap.begin()->first;
			m_frameMap.erase(m_frameMap.begin());
		}
		else if (m_displayLinkHostStartTime)
		{
			uint64_t displayLinkHostRunTime = timeStamp->hostTime - m_displayLinkHostStartTime;
			uint64_t frameTime = m_displayLinkStreamStartTime + displayLinkHostRunTime;

			frame_map_t::iterator it = m_frameMap.upper_bound(frameTime);
			if (it != m_frameMap.begin())
			{
				it--;
				
				pixelBuf = it->second;

				while (m_frameMap.begin() != it)
				{
					fprintf(stderr, "latest %llu, chose %llu, dropping old frame %llu\n", frameTime, it->first, m_frameMap.begin()->first);
					::CVPixelBufferRelease(m_frameMap.begin()->second);
					m_frameMap.erase(m_frameMap.begin());
				}

				m_frameMap.erase(it);
			}
		}
	}

	[m_frameMapLock unlock];
	
	if (!pixelBuf)
		return NO;

	[m_lock lock];

	[self putPixbufInTexture:pixelBuf];
	::CVPixelBufferRelease(pixelBuf);

	[m_lock unlock];
	
	return YES;
}

- (CVReturn)displayFrame:(const CVTimeStamp*)timeStamp
{
	CVReturn			ret;
	NSAutoreleasePool*	pool;
	
	pool = [[NSAutoreleasePool alloc] init];
	
	if ([self getFrameForTime:timeStamp])
	{
		[self drawRect:NSZeroRect];
		ret = kCVReturnSuccess;
	}
	else
	{
		if (m_needsReshape)
		{
			[self drawRect:NSZeroRect];
		}
		ret = kCVReturnError;
	}
	
	[pool release];
	
	return ret;
}

- (void)drawRect:(NSRect)theRect
{
	[m_lock lock];
	{
		[[self openGLContext] makeCurrentContext];
		
		if (m_needsReshape)
		{
			NSRect      frame = [self frame];

			[self update];
			
			if(NSIsEmptyRect([self visibleRect]))
			{
				glViewport(0, 0, 1, 1);
			}
			else
			{
				glViewport(0, 0, GLsizei(frame.size.width), GLsizei(frame.size.height));
			}
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
			glMatrixMode(GL_PROJECTION);
			glLoadIdentity();
			
			m_needsReshape = NO;
		}		
		
		glClearColor(0.0, 0.0, 0.0, 0.0);
		glClear(GL_COLOR_BUFFER_BIT);
		
		// Render frame
		// Draw the entire texture image
		glColor3f(1.0f, 1.0f, 1.0f);
		glEnable(GL_TEXTURE_RECTANGLE_EXT);
		
		glBindTexture(GL_TEXTURE_RECTANGLE_EXT, m_textureId);
		
		glBegin(GL_QUADS);
			glTexCoord2i(0,					m_textureHeight);	glVertex2f(-1, -1);
			glTexCoord2i(m_textureWidth,	m_textureHeight);	glVertex2f(1, -1);
			glTexCoord2i(m_textureWidth,	0);					glVertex2f(1, 1);
			glTexCoord2i(0,					0);					glVertex2f(-1, 1);
		glEnd();
		
		glDisable(GL_TEXTURE_RECTANGLE_EXT);
		
		glFlush();
	}
	[m_lock unlock];
}

- (void)startPlayback
{
	[self clearFrameQueue];
	m_lastFrameDisplayTime = 0;
	m_displayLinkHostStartTime = 0;
	m_displayLinkStreamStartTime = 0;
	
	::CVDisplayLinkStart(m_displayLink);
}

- (void)stopPlayback
{
	::CVDisplayLinkStop(m_displayLink);
	
	[self clearFrameQueue];
	m_lastFrameDisplayTime = 0;
	m_displayLinkHostStartTime = 0;
	m_displayLinkStreamStartTime = 0;
	m_firstFrameArrivalTime = 0;
}

@end
