/*
 * Copyright 2024, ttcoder
 * All rights reserved. Distributed under the terms of the MIT license.
 */


// Genode native *play-record-mixer* back-end for:
// BSoundPlayer


// base.lib.a
#include <base/attached_ram_dataspace.h>  // member
#include <play_session/connection.h>

// libroot.so
#include <OS.h>  // thread_id

// libmedia.so
#include "_ge_consumer_base.h"  // Ancestor



#error /// try without AV resampler:
/*
* get rid of av_resampler, just adapt samples format (float) ?
* HDA (bsd) driver: needs us to sample at 48000, not 44100 ?  What's the recplay_mixer output like ?
* try OSS again in next release (24.04) ?

_d_ diagnostic hack:
* memset:
	//if( framesSubmitted < 44100*15 )
	//	memset( tmp   +   0, '\0', got/2 );
	//else
	//	memset( tmp + got/2, '\0', got/2 );
	//////////
*/

///change period from 10 to 20 ms (50Hz instead of 100) -- requires clean-up/refactoring of the whole file:
enum { frames_per_second_hz = 44'100 };
enum { frames_per_period = frames_per_second_hz/100 /*1100*/ };  // 441 @ 100 Hz = 44100 frames/stereo-samples per second
///const int period_ms = _period_ms;//1000 / 40;  // 25 ms



class hog::ConsumerNative : public ConsumerBase
{
	// Consumes audio frames produced by FramesProducer,
	// sends them to Play service (that is, record_play_mixer)

public:

		ConsumerNative( Genode::Env & env, Genode::Mutex & mutex_ref );
		~ConsumerNative();
		
		// ConsumerBase
		bool IsAlmostFull() override;
		void ConsumeBuffer( Frame_data & frame_data ) override;
		void Halt() override;
		virtual uint64_t Progress_ms();
		
		// New
		bool IsTearingDown() const;



private:  // Code

		void submitPeriod( Frame_data & frame_data );
		void pushOneStereoPeriod( const float * const decoded_interleaved );
public: ///
		void handleTimer();


private:  // Data

		// local data
		Genode::Attached_ram_dataspace 
			writeBuf;
		Genode::Constructible<Play::Connection>
			outChannels[NUM_CHANNELS];
		Play::Time_window _time_window { };
		const unsigned _period_ms; //periodDuration_ms
		
		// stats
		uint64_t framesSubmitted;
		
		// threading, timing
public: ///
		Timer::Connection
			_timer; //+timerConn..
#if 0
		Genode::Signal_handler<hog::ConsumerNative>
			_timer_handler;
#else
		// Using an ep/timer locks up hard (same phenomenon as when we were
		// using AudioOut_oss et al) so hack together a thread-based polling system instead:
		thread_id submitThread;
		bool tearingDown;
#endif
		
		// remote/sync
		Frame_data * frameDataRef;
		Genode::Mutex & feedLockRef;
};


static
status_t thr_entry_poller( void * ptr )
{
	if( nullptr == ptr )
	{
		Genode::error( "NULL ConsumerNative ptr" );
		return -1;
	}
	
	auto obj = static_cast<hog::ConsumerNative*>( ptr );
	
	bigtime_t monotonic_landmark = obj->_timer.elapsed_us();
	// null out the last few zeros, for tracing/diagnostic aesthetics:
	monotonic_landmark /= 1000;
	monotonic_landmark *= 1000;
	
	while( false == obj->IsTearingDown() )
	{
		//Genode::log( "   ", obj->_timer.elapsed_ms(), " ms elapsed (starting to submit)" );
		obj->handleTimer();
		//Genode::log( "   ", obj->_timer.elapsed_ms(), " ms elapsed" );
			//	snooze(9000);
			//	snooze(900);
			// The snooze duration is not enforced, when I specify 900 it
			// still sleeps for 100'000 or some such (!)
			// So use msleep() instead:
#if 0
		obj->_timer.msleep(9);//10 );
#else
		// period relative, not snooze-mic relative ! :
		monotonic_landmark += 10'000; //_d_: hog::snooze_micros;
		bigtime_t delta = 0;
		{
			delta = monotonic_landmark;
			delta -= obj->_timer.elapsed_us();
			
			if( delta < 0 )
			{
				Genode::warning( "mixer feeding not fast enough -- next timer fire already past: ", delta );
				delta = 0;
			}
		}
		//Genode::log("usleep ", delta, " from lmark: ", monotonic_landmark, " and elaps: ", obj->_timer.elapsed_us());
		obj->_timer.usleep( delta );  // delta may be significantly smaller than the nominal 10'000 µs
#endif
	}
	
	return B_OK;
}



hog::ConsumerNative::ConsumerNative( Genode::Env & env, Genode::Mutex & mutex_ref )
:	ConsumerBase( env )
	,writeBuf( env.ram(), env.rm(), frames_per_period * NUM_CHANNELS * sizeof(float) )///
	//,sigHandler( env.ep(), *this, &hog::ConsumerNative::handle_audio_out_progress )
	,outChannels()
	,_time_window( {} )
	,_period_ms( 10 )///25 )// 5 )
	,framesSubmitted( 0 )
	,_timer( env )
#if 0
	,_timer_handler( env.ep(), *this, &hog::ConsumerNative::handleTimer )
#else
	,submitThread( -1 )
	,tearingDown( false )
#endif
	,frameDataRef( nullptr )
	,feedLockRef( mutex_ref )
{
	GeTRACE( "native_playmix.Ctor" );
	
	// build channels
	for( int i = 0; i < NUM_CHANNELS; i++ )
	{
		static const char *names[] = { "left", "right" };//+static_assert re. NUM_CHANNELS..
		
		try
		{
			outChannels[i].construct( env, names[i] );
			
			rl.log( " created channel " ); rl.log( i );
		}
		catch( ... )
		{
			Genode::error( "cannot create audio-out Play #", i, " aka ", names[i] );
			throw;
		}
	}
	
#if 0
	_timer.sigh( _timer_handler );
	_timer.trigger_periodic( _period_ms*1000 );
#else
	status_t st = -1;
	submitThread = spawn_thread( thr_entry_poller, "submit-thread", 10, this );
	if( submitThread > 0 )
		st = resume_thread( submitThread );
	if( st != B_OK )
		Genode::error( "audio_out: could not spawn/resume submit-thread" );
#endif
	
	rl.log( " ctor complete; " );
}


hog::ConsumerNative::~ConsumerNative()
{
	GeTRACE( "native_out.DTor" );
	
	// stop streaming to mixer
	Halt();
}



// work-around for Hai-OS's #defines (imported by MediaFile.cpp) interfering with "attached_ram_dataspace.h" (imported by this file)...
/**********************/
hog::ConsumerBase * instantiate_output_consumer( Genode::Env & env, Genode::Mutex & mutex_ref )
{
	return new hog::ConsumerNative( env, mutex_ref );
}
/**********************/


bool hog::ConsumerNative::IsTearingDown() const
{
	return tearingDown;
}


void hog::ConsumerNative::Halt()
{
	// T-
	tearingDown = true;
	///frameData = nullptr ..?  ..while holding the lock
	
	// T0
	wait_for_thread( submitThread, NULL );
	
	// T+
	for( int i = 0; i < NUM_CHANNELS; i++)
	{
		if( outChannels[i].constructed() )
		{
			outChannels[i]->stop();
			///outChannels[i].destruct();
		}
	}
}


void hog::ConsumerNative::handleTimer()
{
#if 0
	static int count = 0;
	Genode::log( "audio_out timer invoked, count #", count++ );
#endif
	
	Genode::Mutex::Guard lock_guard( feedLockRef );
	
	if( frameDataRef )
		submitPeriod( *frameDataRef );
	//else
	//	Genode::log( "consumer: we have not yet been assigned a producer" );
}


bool hog::ConsumerNative::IsAlmostFull()
{
	return false;///
}


uint64_t hog::ConsumerNative::Progress_ms()
{
	return ((float)framesSubmitted / frames_per_second_hz/*Audio_out::SAMPLE_RATE*/) * 1000.;
}


///++ Consume"Frames"
void hog::ConsumerNative::ConsumeBuffer( Frame_data & frame_data )
{
	//--NO-- don't submit here, we submit in a separate thread/ep, not in the main one--
	//submitPeriod( frame_data )
	
	// Just take note of where to get frames from:
	frameDataRef = &frame_data;
}



void hog::ConsumerNative::submitPeriod( Frame_data & frame_data )
{
	//const unsigned count_frames = frame_data.read_avail() / (NUM_CHANNELS*sizeof(float));
	GeTRACE( " ConsumeBuffer: need to tx ", frame_data.read_avail(), " bytes" );// (", count_frames, " frames)" );
	
	if( frame_data.read_avail() <= 0 )
	{
		rl.log( " *native sink: empty production, return* " );
		rl.Dump();
		//
		return;
		//
	}
	
	rl.log( frame_data.read_avail() ); rl.log( " bytes in fifo; " );
	
	float * tmp = writeBuf.local_addr<float>();
	
	// loop until we exhaust <frame_data> or until we reach the desired buffering level
	//
#if 1
	const int wanted = frames_per_period * NUM_CHANNELS * sizeof(float);
#else
	const int wanted = frames_per_period * NUM_CHANNELS * sizeof(short);
#endif
	
	if/*while*/( frame_data.read_avail() >= wanted
	//		&& false==IsAlmostFull()
		)
	{
		size_t const got = frame_data.read( tmp, wanted );
		rl.log( "\n  pop " ); rl.log( got ); rl.log( " bytes; " );
		//rl.Dump();
		
		if( got != wanted )
		{
			rl.log( " ** got != wanted--AUDIO_OUT_PACKET_SIZE ** " );
			Genode::warning( "retrieved fewer frames than expected" );
		//	rl.Dump();
		//	break;
		}
		
		pushOneStereoPeriod( tmp );
		
		framesSubmitted += frames_per_period;
		//Genode::log( "  framesSubmitted + ", int(frames_per_period), " --> ", framesSubmitted );
	}
	else
		Genode::warning( "CAN'T READ one period's worth of samples, not yet loaded from disk ?" );
}


void hog::ConsumerNative::pushOneStereoPeriod( const float * const decoded_interleaved )
{
	_time_window = outChannels[ LEFT ]->schedule_and_enqueue(
		_time_window,
		{ _period_ms*1000 },
		[&] (auto &submit) {
				for( int i = 0; i < frames_per_period; i++ )
				{
#if 1
					submit( decoded_interleaved[ i * NUM_CHANNELS +LEFT ] );
#else
					auto shorties = (const short*)decoded_interleaved;
					float val = shorties[ i * NUM_CHANNELS +LEFT ] / 32760.;
					submit( val );
#endif
				}
		}
		);
	
	outChannels[ RIGHT ]->enqueue(
		_time_window,
		[&] (auto &submit)
			{
				for( int i = 0; i < frames_per_period; i++ )
					submit( decoded_interleaved[ i * NUM_CHANNELS +RIGHT ] );
//					submit( 0);
			}
		);
}


