/*
	Copyright (C) 2021-2026 Mark Tyler

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program in the file COPYING.
*/

#ifndef MTGIN_SDL_H_
#define MTGIN_SDL_H_

#include <SDL.h>

#include <mtkit.h>
#include <mtpixy.h>



//	mtGin = Mark Tyler's Graphical Interface Nexus



// Functions return: 0 = success, NULL = fail; unless otherwise stated.



#ifdef __cplusplus
extern "C" {
#endif

// C API

typedef struct GinRenderWork	GinRenderWork;



enum	// Object types
{
	GIN_OBJECT_TYPE_ERROR		= -1,
	GIN_OBJECT_TYPE_OK		= 0,

	GIN_OBJECT_SIGNAL_MAX		= 16,	// Per object limit
	GIN_OBJECT_SIGNAL_ID_MAX	= 1000000,

	// Core object types
	GIN_OBJECT_TYPE_OBJECT,
	GIN_OBJECT_TYPE_WINDOW,

	GIN_OBJECT_TYPE_USER		= 1000000
};



enum	// Signal types
{
	GIN_SIGNAL_ERROR		= -1,
	GIN_SIGNAL_OK			= 0,

	// Core signal types
	GIN_SIGNAL_DESTROY_OBJECT,		// About to be destroyed
	GIN_SIGNAL_DESTROY_WINDOW,		// Destroyed
	GIN_SIGNAL_SDL_EVENT,			// ptr=(ObjectData *)

	GIN_SIGNAL_USER			= 1000000
};



enum	// Misc bit flags & other values
{
	GIN_FPS_MIN			= 1,
	GIN_FPS_DEFAULT			= 60,
	GIN_FPS_MAX			= 240,

	GIN_WINDOW_CANVAS_2D		= 0,
	GIN_WINDOW_CANVAS_OPENGL	= 1,

	// Default minimums.  Caller can raise them via window_size_min().
	GIN_WINDOW_WIDTH_MIN		= 100,
	GIN_WINDOW_HEIGHT_MIN		= 100
};



#ifdef __cplusplus
}

// C++ API

#include <atomic>



namespace mtGin
{

/* NOTES:
All audio, video, functions and classes can only be used after the
App singleton object is created via SDL.
Once this App object is destroyed, everything else must stop also.

Timer callbacks must not touch anything in the GUI thread app!

OpenGL functionality doesn't require an App object, such as for a Qt app.
Whenever using OpenGL always set the context before the calls, and restrict to
the GUI thread.
*/

class App;
class AudioFileRead;
class AudioFileWrite;
class AudioPlay;
class AudioRecord;
class AudioVU;
class CB_Data;
class CB_SDLEvent;
class Callback;
class Object;
class Window;



typedef std::function< int(CB_Data *) >			SignalFunc;
	// 0=Handled, 1=Not handled

typedef std::function< void(SDL_Event const &) >	SDLEventFunc;



/// Misc -----------------------------------------------------------------------

std::string clipboard ();
void clipboard_set ( char const * text );

SDL_Surface * surface_from_pixmap ( mtPixmap const * pixmap );



class Callback
{
public:
	Callback ();

	void set ( mtKit::CallbackFunc func );
	void start ();
	void run () const;
	void stop ();

	int is_running () const { return m_active; }

private:
	mtKit::CallbackFunc m_func	= nullptr;
	int		m_active	= 0;
};



class AudioVU
{
public:
	std::vector<int>	m_level;
	uint64_t		m_tick		= 0;
};



class AudioFileRead
{
public:
	AudioFileRead ();
	~AudioFileRead ();

	int open ( char const * filename, SDL_AudioSpec const & spec ) const;
	size_t read ( short * buf, size_t buflen ) const;
		// Returns number of frames (channel * short)

private:
	class Op;	// Opaque / Pimpl
	std::unique_ptr<Op> const m_op;

	MTKIT_RULE_OF_FIVE( AudioFileRead )
};



class AudioFileWrite
{
public:
	AudioFileWrite ();
	~AudioFileWrite ();

	int open ( char const * filename, SDL_AudioSpec const & spec ) const;
	int write ( short const * buf, size_t buflen ) const;

private:
	class Op;	// Opaque / Pimpl
	std::unique_ptr<Op> const m_op;

	MTKIT_RULE_OF_FIVE( AudioFileWrite )
};



class AudioPlay
{
public:
	AudioPlay ();
	~AudioPlay ();

	// e.g. Dev 0 (-1=default); Stereo, 48kHz, AUDIO_S16SYS, 4096 (85ms).
	int open_device ( int device, SDL_AudioSpec const & spec );
	void set_file ( AudioFileRead * file );

	int queue_data ( short const * buf, size_t buflen );
				// Push raw data to queue (now)
	int queue_flush ();
	int queue_file_data ();	// Push audio file data to queue (if required)
		// 0=Success; 1=Wrong mode; -1=File I/O error (EOF)

	int pause ();
	int resume ();
	void stop ();		// Stop using this device
	void toggle_pause_resume ();

	inline int get_status () const { return m_status; }
	inline AudioVU const & get_vu () const { return m_vu; }
	inline SDL_AudioSpec const & get_spec () const { return m_spec; }

private:
	int		m_status	= SDL_AUDIO_STOPPED;

	SDL_AudioSpec	m_spec;
	SDL_AudioDeviceID m_dev_out	= 0;

	AudioFileRead	* m_file	= nullptr;

	size_t		m_bufframes	= 0;
	Uint32		m_bufbytes	= 0;
	short		* m_buf		= nullptr;

	AudioVU		m_vu;

	MTKIT_RULE_OF_FIVE( AudioPlay )
};



class AudioRecord
{
public:
	AudioRecord ();
	~AudioRecord ();

	// e.g. Dev 0 (-1=default); Stereo, 48kHz, 4096 (85ms).
	// Format should always be AUDIO_S16SYS (if not it will fail!)
	// spec.callback is set by libmtgin.
	int open_device ( int device, SDL_AudioSpec & spec );
	void set_file ( AudioFileWrite * file );

	int pause ();
	int resume ();
	void toggle_pause_resume ();

	int get_status ();
	inline AudioVU const & get_vu () const { return m_vu; }
	inline SDL_AudioSpec const & get_spec () const { return m_spec; }

/// ----------------------------------------------------------------------------

	// NOTE: this is the callback and so is on a worker thread! Not to be
	// called by the app!
	void receive_audio (
		Uint8	const	* stream,
		int		len
		);

private:
	void stop ();			// Stop using this device & file

/// ----------------------------------------------------------------------------

	int		m_status	= SDL_AUDIO_STOPPED;
	int		m_cb_panic	= 0;	// Only callback can set!

	AudioFileWrite	* m_file	= nullptr;

	SDL_AudioSpec	m_spec;
	SDL_AudioDeviceID m_dev_in	= 0;

	AudioVU		m_vu;

	MTKIT_RULE_OF_FIVE( AudioRecord )
};



/* All objects must be created dynamically on the heap and not be held in any
smart pointer-like structures.
All objects are owned by their parents, which should all sit beneath a Window.
*/

class Object
{
public:
	Object();
	virtual ~Object();

/* SIGNALS emitted:
	GIN_SIGNAL_DESTROY_OBJECT	When Object has been deleted
*/

	virtual char const * name() const;

	inline uint64_t id() const { return (uint64_t)this; }

	int emit_signal (
		int sig_type,		// SIGNAL_TYPE_*
		CB_Data * data
		);
		// 0=Not handled, else number of successful callbacks

	int add_signal ( int sig_type, SignalFunc func );
		// =sig_id (or <0=Error)
	int remove_signal ( int sig_id );

	inline void block_signals ( bool const block )
	{
		m_block_signals = block;
	}

	void add_child ( Object * child );

private:
	void destroy_offspring();
	void detach_delete();

/// ----------------------------------------------------------------------------

	struct SignalInfo
	{
		int		type;
		int		id;
		SignalFunc	func;
		SignalInfo	* next;
	};

	class SignalStack
	{
	public:
		SignalStack ();
		~SignalStack ();

		inline SignalInfo const * get () const { return m_sig; }

		int add ( int sig_type, SignalFunc func );
		int remove ( int sig_id );

	private:
		SignalInfo	* m_sig		= nullptr;	// Linked list
		int		m_sig_total	= 0;
		int		m_sig_id_next	= 0;

		MTKIT_RULE_OF_FIVE( SignalStack )
	};

	bool		m_block_signals		= false;

	SignalStack	m_sig_stack;

	Object		* m_parent		= nullptr;
	Object		* m_child_first		= nullptr;
	Object		* m_child_last		= nullptr;
	Object		* m_sibling_before	= nullptr;
	Object		* m_sibling_after	= nullptr;

	MTKIT_RULE_OF_FIVE( Object )
};



class CB_Data
{
public:
	virtual ~CB_Data() {}
};



class CB_SDLEvent : public CB_Data
{
public:
	SDL_Event const * sdl_event = nullptr;
};



class Window : public Object
{
public:
	explicit Window (
		int canvas		// GIN_WINDOW_CANVAS_*
		);
	~Window ();

/* SIGNALS emitted:
	GIN_SIGNAL_DESTROY_WINDOW	When SDL Window is about to be deleted
	GIN_SIGNAL_SDL_EVENT		Keyboard, Mouse, etc. handler
*/

	char const * name() const		override;

	void get_geometry ( int &x, int &y, int &w, int &h, Uint32 &flags );
	void set_size_min ( int w, int h );

	void init ( char const * title,
		int x,
		int y,
		int w,
		int h,
		Uint32 flags
		);
		// Throws on error

	SDL_Window * sdl() const { return m_sdl_window; }

	SDL_Renderer * renderer() const { return m_sdl_renderer; }

	enum
	{
		RENDER_THREADS_MIN	= 0,	// 0=Detect CPU threads
		RENDER_THREADS_MAX	= 16,
	};

	// For fast rendering of an mtPixmap to this window
	int render_scale (		// Call once at the beginning
		mtPixmap const * pixmap,
		int scale = 1,		// 1=integer scaling
		int background = 0	// Packed RGB (for any border)
		);

/*
	RGB pixmaps and multi threads are slower than a single thread due to a
	single memcpy being faster than many, Indexed images benefit from multi
	threads due to processing involved (sharing hardware threads 50/50 being
	the best approach, 12 threads total = 6 workers + 6 renderers)
*/
	int render_init_threads ( int threads );	// 0=Detect CPU threads
	int render_pixmap ( mtPixmap const * pixmap, int present = 1 );
	int render_texture ();

	void set_opengl_current () const
	{
		SDL_GL_MakeCurrent ( m_sdl_window, m_glcontext );
	}

	int set_icon ( mtPixmap const * pixmap );
	int set_icon ( char const * filename );

	mtPixmap * dump_to_pixmap () const;

/// ----------------------------------------------------------------------------

	Callback	m_render;

private:
	// This hack is needed on X11 so we can restore the position correctly
	Uint32 border_store_hack ();	// Set border off
	Uint32 border_restore_hack ();	// Restores previous border setting

/// ----------------------------------------------------------------------------

	SDL_Window	* m_sdl_window		= nullptr;
	SDL_GLContext	m_glcontext;
	SDL_Renderer	* m_sdl_renderer	= nullptr;
	Uint32		m_border_hack		= 0;
	int	const	m_canvas;

	SDL_Texture	* m_texture		= nullptr;
	int		m_texture_bpp		= 0;
	int		m_texture_w		= 0;
	int		m_texture_h		= 0;

	using RenderBatch = mtKit::ThreadBatch< GinRenderWork >;

	std::unique_ptr< RenderBatch >	m_batch_render;
};



/// App ------------------------------------------------------------------------

/* The App:
	- starts/stops SDL
	- nexus for windows
	- main loop marshalling of events to the windows or through the "App"
	- frame timing (FPS: set max or gets current rate)
	- audio
*/

class App
{
public:
	explicit App ( Uint32 flags );	// SDL init flags
	~App ();

	void set_fps_max (
		int	fps		// GIN_FPS_MIN..GIN_FPS_MAX
		)
	{
		m_clock_frame.set_fps_max ( fps );
	}

	double get_fps () const { return m_clock_frame.get_fps(); }

	void main_loop ();		// Call only once at the start of app
	void main_loop_iterate ();
	void stop_main_loop() { m_quit = 1; }

	void window_add ( Window & window );

	// Event handler for non-window events like joysticks, controllers, etc.
	void set_sdl_event_callback ( SDLEventFunc const func )
	{
		m_sdl_event_cb = func;
	}

/// ----------------------------------------------------------------------------

	// Called at the beginning of each main loop iteration
	Callback	frame_callback;

private:
	void main_loop ( int loop );
	void poll_events ();

	void frame_time_delay ();
	mtGin::Window * get_window_from_sdl ( SDL_Window * sdl );
	mtGin::Window * get_window_from_id ( Uint32 id );
	Uint32 get_window_event_id ( Uint32 id );

/// ----------------------------------------------------------------------------

	std::map<SDL_Window *, mtGin::Window *> m_window_map;

	mtKit::ClockFrame	m_clock_frame;

	Uint32		m_win_id_old		= 0;
	SDLEventFunc	m_sdl_event_cb		= nullptr;

	int		m_main_loop_nest	= 0;
	int		m_quit			= 0;

	MTKIT_RULE_OF_FIVE( App )
};



}		// namespace mtGin



#endif		// C++ API



#endif		// MTGIN_SDL_H_

