
// ml - A media library representation.

// Copyright (C) 2005-2006 VM Inc.
// Released under the LGPL.
// For more information, see http://www.openlibraries.org.

#include <qvaluelist.h>
#include <qfile.h>
#include <cmath>

#include <boost/thread/thread.hpp>
#include <boost/thread/condition.hpp>

#include <GL/glew.h>

#include <openpluginlib/pl/openpluginlib.hpp>
#include <openpluginlib/pl/GL_utility.hpp>

#include <openmedialib/ml/ml.hpp>

#include <qgl.h>
#include <qtimer.h>
#include <qdragobject.h>
#include <qmime.h>
#include <qstrlist.h>
#include <qevent.h>
#include <qcursor.h>
#include <cmath>
#include <iostream>
#include <deque>

namespace ml  = olib::openmedialib::ml;
namespace il  = olib::openimagelib::il;
namespace opl = olib::openpluginlib;

class QtPlayer : public QGLWidget
{
	Q_OBJECT

public:
	QtPlayer( const std::string &input, QWidget *parent=0, char *name=0 ) 
		: QGLWidget( parent, name, 0, Qt::WDestructiveClose )
		, mouse_moved_( 1 )
		, speed_( 1 )
		, accept_move_( false )
		, accept_jog_( false )
		, old_phy_w_( 0 )
		, old_phy_h_( 0 )
		, old_speed_( 0 )
		, last_frame_( -1 )
		, volume_( 1.0f )
		, deinterlace_mode_( 1 )
	{
		// Create the audio store (openal ignores the frame here)
		ml::frame_type_ptr frame;
		audio_ = ml::create_store( "openal:", frame );
		if ( audio_ && !audio_->init( ) )
			audio_ = ml::store_type_ptr( );

		// Enable mouse tracking and drops
		setMouseTracking( true );
		setAcceptDrops( true );

		// Create a timer to control a/v playout
		timer_ = new QTimer( this );
		connect( timer_, SIGNAL( timeout( ) ), this, SLOT( timeOutSlot( ) ) );

		// Mouse timer hides the mouse after approx 5 seconds when no mouse movement in fullscreen
		mouse_ = new QTimer( this );
		connect( mouse_, SIGNAL( timeout( ) ), this, SLOT( mouseSlot( ) ) );
		mouse_->start( 5000 );

		// Play the input
		if ( !play( input ) )
			play( "jahlogo.jpg/sequence:" );
	}
	
	virtual ~QtPlayer( )
	{
	}

	bool play( const std::string &input, int speed = 1 )
	{
		return play( ml::create_input( input.c_str( ) ), speed );
	}

	bool play( ml::input_type_ptr input, int speed = 1 )
	{
		if ( input != 0 )
		{
			// Pause the current playback
			if ( input_ ) set_speed( 0 );

			// Set everything back to defaults
			input_ = input;
			field_ = ml::frame_type_ptr( );
			last_frame_ = -1;

			// Start the video
			set_speed( speed );
		}
		return input != 0;
	}

	void set_speed( int speed, bool save = true )
	{
		if ( save ) old_speed_ = speed_;
		speed_ = speed;

		timer_->stop( );
		audio_flush( );
		if ( save && input_ ) input_->seek( position( ) );
		preroll_.clear( );

		if ( input_ && speed_ )
		{
			// Make sure we process everything during the preroll
			input_->set_process_flags( ml::process_image | ml::process_audio );

			// Fetch a frame from the input
			ml::frame_type_ptr frame = input_->fetch( );

			// TODO: Parameterise preroll
			for ( int i = 0; speed_ == 1 && i < 4; i ++ )
			{
				visualise( frame );
				audio_play( frame );
				frame->set_image( il::convert( frame->get_image( ), L"b8g8r8" ) );
				preroll_.push_back( frame );
				input_->seek( 1, true );
				frame = input_->fetch( );
			}

			// Delay between timer events
			int delay = 0;

			// If we have have audio, we'll rely on the audio device for clocking, otherwise:
			if ( speed_ == 1 && ( !audio_ || !frame->get_audio( ) ) )
			{
				// Calculate the delay in milliseconds
				delay = int( frame->get_duration( ) * 1000 );

				// Attempt to play out all fields for interlaced sources
				il::image_type_ptr image = frame->get_image( );
				if ( deinterlace_mode_ == 0 && ( image && image->field_order( ) != il::progressive ) )
					delay /= 2;
			}

			// Start the timer
			timer_->start( delay );
		}

		updateGL( );
	}

	int speed( )
	{
		return speed_;
	}

	int position( )
	{
		int pos = 0;
		if ( !preroll_.empty( ) )
			pos = ( *preroll_.begin( ) )->get_position( );
		else if ( input_ )
			pos = input_->get_position( );
		return pos;
	}

	int length( )
	{
		int len = 0;
		if ( input_ ) len = input_->get_frames( );
		return len;
	}

	void toggle_full_screen( )
	{
		if ( isFullScreen( ) ) 
			showNormal( ); 
		else 
			showFullScreen( ); 
		setCursor( QCursor( Qt::ArrowCursor ) );
	}

	float volume( )
	{
		return volume_;
	}

	void set_volume( float volume )
	{
		volume_ = volume;
	}

protected:
    void dragEnterEvent( QDragEnterEvent *event )
    {
    	event->accept( true );
    }

	void dropEvent( QDropEvent *event )
	{
		QStringList list;
		if ( QUriDrag::decodeLocalFiles( event, list ) )
			if ( !play( list[ 0 ].ascii( ) ) )
				play( ( list[ 0 ].left( list[ 0 ].findRev( "/" ) + 1 ) + 
						QString( "*" ) +
						list[ 0 ].mid( list[ 0 ].findRev( "." ) ) + 
						QString( "/sequence:" ) ).ascii( ), 0 );
	}

	void restore_speed( )
	{
		set_speed( old_speed_ );
	}

	void mousePressEvent( QMouseEvent *e )
	{
		if ( e->type( ) == QEvent::MouseButtonDblClick )
		{
			toggle_full_screen( );
		}
		else if ( e->button( ) & Qt::MidButton )
		{
			set_speed( !speed_ );
		}
		else if ( e->button( ) & Qt::RightButton )
		{
			accept_move_ = true;
			set_speed( 0 );
			mouseMoveEvent( e );
		}
		else if ( e->button( ) & Qt::LeftButton )
		{
			accept_jog_ = true;
			jog_x_ = e->x( );
			set_speed( 0 );
		}
	}

	void mouseMoveEvent( QMouseEvent *e )
	{
		if ( accept_move_ )
		{
			int position = int( double( input_->get_frames( ) * e->x( ) ) / width( ) );
			input_->seek( position );
			set_speed( speed_, false );
		}
		else if ( accept_jog_ )
		{
			int offset = int( std::log( std::fabs( double( jog_x_ - e->x( ) ) ) ) );
			if ( jog_x_ != e->x( ) && offset )
				set_speed( ( e->x() < jog_x_ ? -1 : 1 ) * offset, false );
			else
				set_speed( 0, false );
		}

		if ( isFullScreen( ) )
		{
			if ( mouse_moved_ == -1 )
				setCursor( QCursor( Qt::ArrowCursor ) );
			mouse_moved_ = 1;
		}
	}

	void mouseReleaseEvent( QMouseEvent * )
	{
		if ( accept_move_ || accept_jog_ )
		{
			accept_move_ = false;
			accept_jog_ = false;
			restore_speed( );
		}
	}

	void initializeGL()
	{
		glewInit( );

		ml::frame_type_ptr frame = input_->fetch( );

		if ( frame->get_image( ) != 0 )
		{
			int phy_w, phy_h, req_w, req_h;
			calculate_dimensions( frame, phy_w, phy_h, req_w, req_h );

			float tex_w, tex_h;
			GLenum target;
			if ( opl::texture_target( phy_w, phy_h, target, tex_w, tex_h ) )
			{
				glGenTextures( 1, &texture_id_ );
				glBindTexture( target, texture_id_ );
				glTexParameteri( target, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
				glTexParameteri( target, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
				glTexParameteri( target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
				glTexParameteri( target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
			}
		}
	}
	
	void resizeGL( int w, int h )
	{
		glViewport( 0, 0, w, h );
	}
	
	// Very silly
	void visualise( ml::frame_type_ptr frame )
	{
		if ( frame->get_image( ) == 0 && frame->get_audio( ) != 0 )
		{
			ml::audio_type_ptr audio = frame->get_audio( );
			il::image_type_ptr image = il::allocate( L"b8g8r8", 640, 480 );
			memset( image->data( ), 0, image->size( ) );

			short *buff = ( short * )audio->data( );
			int samples = audio->samples( );
			int channels = audio->channels( );

			double sample_offset = 640.0 / samples;
			int pitch = image->pitch( );
			unsigned char *middle = image->data( ) + pitch * image->height( ) / 2;

			for ( int i = 0; i < 640; i ++ )
			{
				for ( int j = 0; j < channels; j ++ )
				{
					short sample = ( buff + 2 * int( sample_offset * i * channels ) )[ j ] / 140;
					unsigned char *p = middle - sample * pitch + i * 3;
					*p ++ = 0;
					*p ++ = j == 0 ? 255 : 0;
					*p ++ = j == 0 ? 0 : 255;
				}
			}

			frame->set_image( image );
		}
	}

	void overlay( ml::image_type_ptr image )
	{
		if ( length( ) > 1 && ( speed_ == 0 || accept_move_ || accept_jog_ ) )
		{
			int width = int( 0.8 * image->width( ) );
			int used = int( double( width * ( position( ) + 1 ) ) / length( ) + 0.5 );
			int height = int( 0.05 * image->height( ) );
			unsigned char *buffer = image->data( ) + height * 18 * image->pitch( ) + int( image->width( ) * 0.1 - 1 ) * 3;
			memset( buffer, 255, width * 3 );
			buffer += image->pitch( );
			memset( buffer, 255, width * 3 );
			buffer += image->pitch( );
			for ( int i = 2; i < height; i ++ )
			{
				buffer[ 0 ] = buffer[ 1 ] = buffer[ 2 ] = 255;
				buffer[ 3 ] = buffer[ 4 ] = buffer[ 5 ] = 255;
				memset( buffer, 255, used * 3 );
				buffer[ width * 3 - 6 ] = buffer[ width * 3 - 5 ] = buffer[ width * 3 - 4 ] = 255;
				buffer[ width * 3 - 3 ] = buffer[ width * 3 - 2 ] = buffer[ width * 3 - 1 ] = 255;
				buffer += image->pitch( );
			}
			memset( buffer, 255, width * 3 );
			buffer += image->pitch( );
			memset( buffer, 255, width * 3 );
		}
	}

	ml::frame_type_ptr levels( ml::frame_type_ptr frame )
	{
		ml::audio_type_ptr audio = frame->get_audio( );
		if ( audio )
		{
			int samples = audio->samples( ) * audio->channels( );
			short int *ptr = ( short int * )( audio->data( ) );
			while ( samples -- )
			{
				*ptr = short( volume_ * *ptr );
				ptr ++;
			}
		}
		return frame;
	}

	void paintGL()
	{ 
		int flags = 0;

		if ( speed_ == 1 && audio_ && audio_->empty( ) )
			flags = ml::process_audio;
		else
			flags = ml::process_image | ml::process_audio;

		input_->set_process_flags( flags );

		ml::frame_type_ptr frame = field_ ? field_ : input_->fetch( );

		if ( frame == 0 ) return;

		if ( !field_ )
		{
			if ( flags & ml::process_image )
				visualise( frame );

			if ( speed_ == 1 )
			{
				if ( audio_ ) audio_->push( levels( frame ) );
				preroll_.push_back( frame );
				frame = *( preroll_.begin( ) );
				preroll_.erase( preroll_.begin( ) );
			}
		}

		ml::image_type_ptr image = frame->get_image( );

		if ( image != 0 )
		{
			glDisable( GL_DEPTH_TEST );
			glDepthMask( 0 );

			glClear( GL_COLOR_BUFFER_BIT );

			glPixelStorei( GL_UNPACK_ALIGNMENT, 4 );
			
			int phy_w, phy_h, req_w, req_h;
			calculate_dimensions( frame, phy_w, phy_h, req_w, req_h );

			std::pair<int, GLenum> glformat = opl::pf_to_gl_format( image->pf( ).c_str( ) );
			
			ml::image_type_ptr new_im;
			// Convert to a format which (hopefully) the GPU knows
			if( !glformat.first )
			{
				new_im = il::convert( image, L"b8g8r8" );

				glformat.first  = 3;
				glformat.second = GL_BGR_EXT;
			}
			else
				new_im = image;

			GLenum type = GL_UNSIGNED_BYTE;
			if( new_im->pf( ) == L"yuv422" )
				type = GL_UNSIGNED_SHORT_8_8_APPLE;

			float tex_w, tex_h;
			GLenum target;
			if ( opl::texture_target( phy_w, phy_h, target, tex_w, tex_h ) )
			{
				// Conform to cropped and not flipped or flopped (optimal for video playout)
				new_im = il::conform( new_im, il::cropped );

				// Allow frame reuse (for field displays for example)
				frame->set_image( new_im );

				// Determine if we should be showing field 0, 1 or the whole frame
				if ( image->field_order( ) != il::progressive )
				{
					if ( deinterlace_mode_ == 0 )
					{
						new_im = il::field( new_im, !field_ && last_frame_ <= frame->get_position( ) ? 0 : 1 );
						field_ = field_ ? ml::frame_type_ptr( ) : frame;
						last_frame_ = frame->get_position( );
					}
					else if ( deinterlace_mode_ == 1 )
					{
						new_im = il::deinterlace( new_im );
					}
				}

				// TODO: Move the overlay to a seperate texture (ie: fix position relative 
				// to the GL widget as opposed to relative to an arbitrary frame size)
				overlay( new_im );
				
				glEnable( target );
				glBindTexture( target, texture_id_ );
				if ( phy_w != old_phy_w_ || phy_h != old_phy_h_ )
				{
					glTexParameteri( target, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
					glTexParameteri( target, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
					glTexParameteri( target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
					glTexParameteri( target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
					glTexImage2D( target, 0, glformat.first, phy_w, phy_h, 0, glformat.second, type, new_im->data( ) );
					old_phy_w_ = phy_w;
					old_phy_h_ = phy_h;
				}
				else
				{
					glTexSubImage2D( target, 0, 0, 0, phy_w, phy_h, glformat.second, type, new_im->data( ) );
				}

				glMatrixMode( GL_PROJECTION );
				glPushMatrix( );
				glLoadIdentity( );
				gluOrtho2D( 0.0f, width( ), 0.0f, height( ) );
			
				glMatrixMode( GL_MODELVIEW );
				glPushMatrix( );
				glLoadIdentity( );

				float off_x = 0.5 * ( width( ) - req_w );
				float off_y = 0.5 * ( height( ) - req_h );
				glTranslatef( off_x, off_y, 0.0f );

				float zoom_w = ( float ) req_w / ( float ) phy_w; 
				float zoom_h = ( float ) req_h / ( float ) phy_h;
				glScalef( zoom_w, zoom_h, 1.0f );

				glBegin( GL_QUADS );
				glTexCoord2f( 0.0f, 0.0f );
				glVertex2f( 0.0f, phy_h );
				glTexCoord2f( tex_w, 0.0f );
				glVertex2f( phy_w, phy_h );
				glTexCoord2f( tex_w, tex_h );
				glVertex2f( phy_w, 0.0f );
				glTexCoord2f( 0.0f, tex_h );
				glVertex2f( 0.0f, 0.0f );
				glEnd( );

				glMatrixMode( GL_MODELVIEW );
				glPopMatrix( );

				glMatrixMode( GL_PROJECTION );
				glPopMatrix( );

				glDisable( target );
			}
			else
			{
				// TODO: Correct for the interlaced case

				// Since we don't have a texture, crop, flip and don't flop
				new_im = il::conform( new_im, il::cropped | il::flipped );
				frame->set_image( new_im );

				// Display the image at the correct aspect ratio and scaled to fill the core as much as possible
				glPixelZoom( ( ( float )req_w / phy_w ), ( ( float )req_h / phy_h ) );
   				glDrawPixels( phy_w, phy_h, glformat.second, type, new_im->data( ) );
			}
			
			glDepthMask( 1 );
			glEnable( GL_DEPTH_TEST );
		}

#	ifndef NDEBUG
		GLenum error;
		while( ( error = glGetError( ) ) != GL_NO_ERROR )
			fprintf( stderr, "GLerror:%s\n", gluErrorString( error ) );
#	endif

	}
	
	void keyPressEvent( QKeyEvent *e )
	{
		switch( e->key() )
		{
			case Qt::Key_Escape: close(); break;
			case ' ': set_speed( !speed_ ); break;
			case Qt::Key_Up: set_volume( volume( ) + ( volume( ) < 1.0f ? 0.1f : 0.0f ) ); break;
			case Qt::Key_Down: set_volume( volume( ) - ( volume( ) > 0.0f ? 0.1f : 0.0f ) ); break;
			case Qt::Key_Left: set_speed( 0 ); input_->seek( -1, true ); set_speed( 0 ); break;
			case Qt::Key_Right: set_speed( 0 ); input_->seek( 1, true ); set_speed( 0 ); break;
			case Qt::Key_PageUp: input_->seek( -1000, true ); set_speed( speed_, false ); break;
			case Qt::Key_PageDown: input_->seek( 1000, true ); set_speed( speed_, false ); break;
			case 'H': input_->seek( -10, true ); set_speed( speed_, false ); break;
			case 'L': input_->seek( 10, true ); set_speed( speed_, false ); break;
			case 'F': toggle_full_screen( ); break;
			case 'N': if ( isTopLevel( ) ) createNewWindow( ); break;
			case 'D': switch_deinterlace_mode( ); break;
		}
	}

	void createNewWindow( )
	{
		( new QtPlayer( "jahlogo.jpg/sequence:" ) )->show( );
	}

protected slots:
	virtual void timeOutSlot()
	{
		if ( speed_ != 0 )
		{
			if ( field_ == 0 )
				input_->seek( speed_, true );
			if ( input_->get_position( ) == input_->get_frames( ) - 1 )
				set_speed( 0 );
			updateGL();
		}
	}

	virtual void mouseSlot()
	{
		if ( isFullScreen( ) )
		{
			if ( !mouse_moved_ )
			{
				setCursor( QCursor( Qt::BlankCursor ) );
				mouse_moved_ = -1;
			}
			else if ( mouse_moved_ != -1 )
			{
				mouse_moved_ = 0;
			}
		}
	}

private:
	void switch_deinterlace_mode( )
	{
		int current = speed_;
		if ( ++ deinterlace_mode_ == 3 )
			deinterlace_mode_ = 0;
		input_->seek( position( ) );
		play( input_, current );
	}

	int deinterlace_mode( )
	{
		return deinterlace_mode_;
	}

	inline void calculate_dimensions( ml::frame_type_ptr frame, int &phy_w, int &phy_h, int &req_w, int &req_h )
	{
		ml::image_type_ptr image = frame->get_image( );
	
		// This is the absolute WxH we have
		int abs_w = width( );
		int abs_h = height( );
	
		// This is the physcial WxH of the image
		double ar = frame->aspect_ratio( );
		phy_w = image->width( );
		phy_h = image->height( );
	
		// In lieu of a deinterlacer, we'll attempt to show both fields
		if ( deinterlace_mode_ == 0 && image->field_order( ) != il::progressive )
			phy_h /= 2;

		// Calculate the required display WxH
		req_h = abs_h;
		req_w = int( req_h * ar );
	
		// Scale up or down to fit
		if ( req_w > abs_w )
		{
			req_w = abs_w;
			req_h = int( req_w / ar );
		}
	}

	void audio_play( ml::frame_type_ptr frame )
	{
		if ( audio_ ) audio_->push( levels( frame ) );
	}

	void audio_flush( )
	{
		if ( audio_ ) audio_->flush( );
	}

private:
	ml::input_type_ptr input_;
	ml::store_type_ptr audio_;
	std::deque < ml::frame_type_ptr > preroll_;
	ml::frame_type_ptr field_;
	GLuint texture_id_;
	QTimer *mouse_;
	int mouse_moved_;
	QTimer *timer_;
	int speed_;
	bool accept_move_;
	bool accept_jog_;
	int jog_x_;
	int old_phy_w_;
	int old_phy_h_;
	int old_speed_;
	int last_frame_;
	float volume_;
	int deinterlace_mode_;
};
