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

// Genode *OSS* back-end for:
// BSoundPlayer


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

// libc.lib.so
#include <fcntl.h>  // ioctl
#include <libc/component.h>  // Libc::with_libc()
#include <sys/soundcard.h>  // ioctl codes: OSS_GETVERSION, SNDCTL_DSP_HALT, etc

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



class hog::ConsumerOss : public ConsumerBase
{
	
public:

		ConsumerOss(
			Genode::Env & env
	//		Genode::Signal_handler<Audio_player::Main>  & sigh//progress_dispatcher;
	//		Genode::Signal_context_capability sigh
			);
		~ConsumerOss();
		
	//	void start_timer();
	//	void stop_timer();
		bool IsAlmostFull() override;
		void ConsumeBuffer( Frame_data & frame_data ) override;
		void Halt() override;
		virtual uint64_t Progress_ms();


private:  // Constants

	//xxx or:  sizeof(short) * NUM_CHANNELS * PERIOD * how_many_periods_to_batch_together
	//enum { BUFSIZE = 128 * 1024 };


private:  // Data

		int fdOss;
		
///ToDo: use a DS, instead of a (smaller/riskier) stack-allocated buf (careful, that class gets "clashed" by hai-libc, make this a separate compilation unit)
#if 0
		Attached_ram_dataspace  writeBuf;
#endif

#if 0
		///ToDo: any way to get a signal, when OSS is ready to receive new buffers ?
		void _handle_timeout( Genode::Duration );
		Timer::Connection _timer;
		Timer::One_shot_timeout<Consumer> _timeout;
		
		//..for invoking parent hook..
		Genode::Signal_handler<Audio_player::Main>  & sigHandler;//Genode::Signal_context_capability  sigHandler;
#endif
};




hog::ConsumerOss::ConsumerOss(
			Genode::Env & env
	//		Genode::Signal_handler<Audio_player::Main>  & sigh//Genode::Signal_context_capability sigh
			)
:	ConsumerBase( env )
	,fdOss( -1 )
#if 0
	,writeBuf( env.ram(), env.rm(), BUFSIZE )
#endif
#if 0
	,_timer( env )
	,_timeout( _timer, *this, &Consumer::_handle_timeout )
	,sigHandler( sigh )
#endif
{
	Genode::log( "oss.Ctor" );
	
	fdOss = open( "/dev/dsp", O_WRONLY );
}

hog::ConsumerOss::~ConsumerOss()
{
	Genode::log( "oss.DTOR  [", fdOss, "]" );
	
//	stop_timer();
	
	if( fdOss >= 0 )
	{
		Halt();
		close( fdOss );
		fdOss = -1;
	}
	
	Genode::log( "oss.DTOR done!" );
}

// 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::ConsumerOss( env );
}
/**********************/


#if 0
void hog::Consumer::start_timer()
{
	//GeTRACE( "oss.start" );
	
	// initiate timer (it will self-sustain afterwards with repeated calls to schedule())
	Genode::Microseconds delay { 9000 };
	_timeout.schedule( delay );
}

void hog::Consumer::stop_timer()
{
	//GeTRACE( "oss.stop" );
	
	// seems to work? Hence no need to use a construct/destruct scheme, nor a boolean "fTearingDown" scheme
	_timeout.discard();
}
#endif


void hog::ConsumerOss::Halt()
{
	Genode::log( "oss.halt...  [", fdOss, "]" );
	
	int res = 0;
	Libc::with_libc([&] ()
	{
		res = ioctl( fdOss, SNDCTL_DSP_HALT );
	});
	
	Genode::log( "  .. oss.halt -> ", res );
}


#if 0
void hog::ConsumerOss::_handle_timeout( Genode::Duration )
{
	//GeTRACE( "oss.handle_timeout" );
	
	// make class AudioPlayer call both its Producer and (our) ConsumeBuffer()
	sigHandler.dispatch( 0 );
	
	//xxx Move to T+ of the taks, since its a time-consuming task and the timer might prematurely fire _before_ the task is complete....?
	
	// sustain timer
	/*
//	Genode::Microseconds delay { 9000 };  // ~100 Hz
	Genode::Microseconds delay { 19000 };  // ~50 Hz
	_timeout.schedule( delay );
	*/
	start_timer();
}
#endif


///static int smp_count = 0;

uint64_t hog::ConsumerOss::Progress_ms()
{
	//int framesSubmitted = smp_count;
	//int frames_per_second_hz = 44100;
	return ((float)framesSubmitted / frames_per_second_hz/*Audio_out::SAMPLE_RATE*/) * 1000.;
}


void hog::ConsumerOss::ConsumeBuffer( Frame_data & frame_data )
{
	GeTRACE( "oss.Consume  [", fdOss, "]" );
	GENODE_LOG_TSC( 200 );
	
	if( fdOss < 0 )
	{
		///later: should use with_libc() for accessing <errno> ?
		Genode::error( "OSS was not opened, fd=", fdOss, " errno=", errno );
		//
		return;
		//
	}
	
#if 0
	// debugging: create square wave "input":
	short buf[ Audio_out::PERIOD ] = { 0 };
	{
		for( unsigned i = 0; i < Audio_out::PERIOD; i++ )
		{
			float tmp_val = 0.;
			
			if( i < Audio_out::PERIOD/2.0 )
				tmp_val = 0.25;
			else
				tmp_val = -0.25;
			
			buf[i] = tmp_val * 32700;
		}
	}
#else
	if( frame_data.read_avail() <= 0 )
	{
		Genode::log( "** oss sink: empty production? failed to decode?" );
		//
		return;
		//
	}
	
#if 0
	if( frame_data.read_avail() > writeBuf.size() )
	{
		Genode::error( "oss-out: buffer overflow, bailing out" );
		//
		return;
		//
	}
#else
	///ToDo: uh ? can this get "too low" (yes: for WAV files eg) or "too high" and overflow ? :
#endif
	unsigned count_frames = frame_data.read_avail() / (NUM_CHANNELS*sizeof(float));
	//Genode::log( "    chunk size: ", count_frames, " frames" );  // typically: 1152 frames

#if 0
	short * buf = writeBuf.local_addr<short>();
	///ToDo: should use a ram_dataspace for tmp[] as well, not just buf[]
#else
	short buf[ count_frames * NUM_CHANNELS ] = { 0 };
#endif
/* ++
	// Iterate to fill output buffer "piecemeal"... It's important to ensure each write() call will send a big enough set of frames, to beat the overhead.
	//while (frame_data.read_avail() > (AUDIO_OUT_PACKET_SIZE))
	while( buf.occupied() +9 < buf.capacity() && frame_data.read_avail() > 0 )
	{
		if(count_frames > 100) count_frames = 100;  e.g.
		remaining_count = z;
		..
*/
	{
		float tmp[ count_frames * NUM_CHANNELS ] = { 0. };
		{
			size_t const n = frame_data.read( tmp, sizeof(tmp) );
			GeTRACE(" oss: got ", n, " bytes (", count_frames, " frames) (", count_frames*NUM_CHANNELS, " samples)");
			;
			if( n != sizeof(tmp) )
				Genode::warning( "retrieved fewer frames than expected" );
		}
		
		//or just a for() loop on count_*Samples* ?
		for( unsigned i = 0; i < count_frames; i++ )
		{
			//GeTRACE( "  conv frame ", i );
			buf[i * NUM_CHANNELS +LEFT]  = tmp[i * NUM_CHANNELS + LEFT]  * 32700.;
			buf[i * NUM_CHANNELS +RIGHT] = tmp[i * NUM_CHANNELS + RIGHT] * 32700.;
		}
	}
#endif
	
	int res = 0;
	
	Libc::with_libc([&] () {
		// This might hang when used against *black_hole* (e.g. in Qemu), if the write rate isn't sustained,
		// after a few seconds BH gives up and write() never returns ; seems rock solid against pci_audio_drv however.
		///ToDo: it seems write() hangs on (bare metal) HDA too, not just with black_hole, in fact,
		// re-enable log() lines to confirm ?
		//UPD: could the "hang" actually be caused by a vfs server crash ? Nope, other apps (AK, Lightning etc) continue to work with vfs, even after AC locks-up.
		//UPD: seems the lock-ups are unrelated to audio/OSS, but to the scheduler thread instead ?
		
		// Heisen-Bug: T410 no longer reproduces the write() hang, after adding this log()s ?
		// UPD: yea it does, though now it takes 2+ hours
	//	Genode::log( "  ..write ", sizeof(buf), " bytes:" );
	//	GENODE_LOG_TSC_NAMED( 600, "oss.write" );
		
		res = write(
			fdOss,
			buf,
			sizeof( buf )
			);
		//Genode::log( "  ..done with write -> ", res );
	});
	
	if( res != signed(sizeof(buf)) )
		Genode::warning( "OSS.write got ", res, " instead of ", sizeof(buf) );
	
	GeTRACE( " consume done!" );
}


bool hog::ConsumerOss::IsAlmostFull()
{
	int res = 0;
	
	//int oss_ver = 0;
	struct audio_errinfo  err_info = {};
	oss_count_t  optr = {};
	struct audio_buf_info  o_inf = {};
	Libc::with_libc([&] ()
	{
		//res = ioctl( fdOss, OSS_GETVERSION, &oss_ver );
		res = ioctl( fdOss, SNDCTL_DSP_GETERROR, &err_info );
		res = ioctl( fdOss, SNDCTL_DSP_CURRENT_OPTR, &optr );
		res = ioctl( fdOss, SNDCTL_DSP_GETOSPACE, &o_inf );
	});
///smp_count = optr.samples;
#if 0
	//Genode::log("ioctl -> ", res, " ver ", oss_ver );
	Genode::log("ioctl -> ", res, " play_underruns ", err_info.play_underruns );
	Genode::log("ioctl -> ", res, " samples ", optr.samples, " fifo_samples ", optr.fifo_samples );
	Genode::log("ioctl -> ", res, " fragments ", o_inf.fragments, " fragstotal ", o_inf.fragstotal, " fragsize ", o_inf.fragsize, " bytes ", o_inf.bytes );
#endif
	int free_perc = -1;
	if( o_inf.fragstotal > 0 )
	{
		free_perc = 100 * o_inf.fragments;
		free_perc /= o_inf.fragstotal;
	}
	GeTRACE( "    audio queue free space: ", free_perc, " %" );
	
	// We draw the line for "almost full" at 10% free space (90% full) : beyond that,
	// we ask the caller to snooze for a bit before submitting more audio data..
	// Might spare us the blockage that occurs when doing "blocking writes" that never un-block?
	// UPD: no change, the lock-ups still occur, but now they occur at 90% full instead :-) :-/
	//
	return free_perc <= 10;
}


