/*****************************************************************************

        GRAOUMF TRACKER 2
        Author: Laurent de Soras, 1996-2016

--- Legal stuff ---

This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.

*****************************************************************************/



/*\\\ FICHIERS INCLUDE \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/

#include	<assert.h>
#include	<stdlib.h>

#include	"base.h"
#include	"base_ct.h"
#include	"d2d.h"
#include	"file.h"
#include	"gt_limit.h"
#include	"gtracker.h"
#include	"log.h"
#include	"memory.h"
#include	"Pattern.h"
#include	"player.h"
#include	"Thread.h"
#include	"WaveForm.h"



/*\\\ CONSTANTES PRIVEES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/

#define	D2D_DEFAULT_D2D_CACHE_SIZE	(512*1024L)	/* Taille par defaut du cache en octets */



/*\\\ TYPES & STRUCTURES PRIVEES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/



/*\\\ PROTOTYPES DES FONCTIONS PRIVEES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/

void	D2D_cache_manager (void);

void	D2D_update_track (int track_index);
void	D2D_update_d2d_cache_cell (Voice &voice);



/*\\\ VARIABLES EXTERNES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/

/* Pointeur sur le handle du thread du gestionaire du cache. */
Thread *	D2D_thread_handle_ptr = NULL;



/*\\\ VARIABLES PRIVEES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/

/* true indique au gestionnaire de cache qu'il doit quitter. Quand l'ordre a
   ete recu, le flag revient a false. */
volatile bool	D2D_cache_manager_quit_flag;



class D2D_ThreadStub
:	public ThreadCbInterface
{
public:
	// ThreadCbInterface
	virtual void	do_run_thread ()
	{
		D2D_cache_manager ();
	}
};

D2D_ThreadStub	D2D_cache_mgr_thread_stub;



/*\\\ FONCTIONS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/



/*==========================================================================*/
/*      Nom: D2D_init                                                       */
/*      Description: Initialise la gestion du cache du Direct-2-Disk.       */
/*      Retour: 0 si tout s'est bien passe.                                 */
/*==========================================================================*/

signed int	D2D_init (void)
{
	/* Installe le thread de gestion du cache */
	D2D_cache_manager_quit_flag = false;
	D2D_thread_handle_ptr = new Thread (D2D_cache_mgr_thread_stub);
	if (D2D_thread_handle_ptr->spawn ())
	{
		LOG_printf ("D2D_init: Error: couldn't spawn a thread for D2D management.\n");
		return (-1);
	}

	return (0);
}



void	D2D_restore (void)
{
	if (D2D_thread_handle_ptr != NULL)
	{
		D2D_cache_manager_quit_flag = true;
		delete D2D_thread_handle_ptr;
		D2D_thread_handle_ptr = NULL;
	}
}



/*==========================================================================*/
/*      Nom: D2D_cache_manager                                              */
/*      Description: Thread qui s'occupe de surveiller les buffers dans le  */
/*                   cache et de les recharger si necessaire.               */
/*==========================================================================*/

void	D2D_cache_manager (void)
{
	for ( ; ; )
	{
		/* Gestion du cache */
		D2D_cache_manager_routine ();

		/* Regarde si on doit sortir */
		if (D2D_cache_manager_quit_flag)
		{
			break;
		}

		/* Met le thread en pause pendant quelques temps */

		/*** A faire: calculer un temps raisonnable ***/

		Thread::sleep (50);
	}

	D2D_cache_manager_quit_flag = false;
}



/*==========================================================================*/
/*      Nom: D2D_cache_manager_routine                                      */
/*      Description: Routine qui s'occupe de surveiller les buffers dans le */
/*                   cache et de les recharger si necessaire.               */
/*      Retour: 0 si tout s'est bien passe, ou -1 si une erreur a eu lieu   */
/*              (ces erreurs ne doivent pas etre prises en compte si le     */
/*              player fonctionne en temps reel).                           */
/*==========================================================================*/

signed int	D2D_cache_manager_routine (void)
{
	int		buffer_cnt;
	int		voice_index;
	int		sample_mul;
	signed int	ret_val;
	BYTE *	buffer_ptr;
	SLWORD	buffer_len;
	FILE *	file_ptr;

	ret_val = 0;

	{
		/* Signale qu'on met a jour les descripteurs et qu'il ne faut pas
			modifier les donnees des samples en ce moment. */
		std::lock_guard <std::mutex>	lock (GTK_mutex);

		/* Met a jour toutes les voix */
		for (int track = 0; track < GTK_nbr_tracks [Pattern_TYPE_SPL]; track ++)
		{
			D2D_update_track (track);
		}
	}	/* C'est bon maintenant */

	/* Scanne toutes les entrees du cache */
	for (voice_index = 0; voice_index < GTK_nbr_tracks [Pattern_TYPE_SPL]; voice_index ++)
	{
		Player &			player = Player::use_instance ();
		Voice &			voice = player._voice_arr [voice_index];
		D2dBuffer &		desc = voice.spl.d2d._desc;

		if (desc.buffer_ptr [0] == NULL)
		{
			continue;
		}

		/* Si les donnees ne correspondent pas a l'entree, on met a jour. */
		for (buffer_cnt = 0; buffer_cnt < 2; buffer_cnt ++)
		{
			if (! desc.buf_flag [buffer_cnt])
			{
				int64_t        file_pos = desc.buf_pos [buffer_cnt];
				sample_mul = voice.spl.resol * voice.spl.tracks;
				buffer_ptr = (BYTE *) desc.buffer_ptr [buffer_cnt];
				buffer_len = desc.buf_length [buffer_cnt];
				buffer_len = MIN (
					buffer_len,
					voice.spl.reppos + voice.spl.replen - desc.buf_pos [buffer_cnt]
				);
				if (desc.buf_pos [buffer_cnt] < 0)
				{
					file_pos -= desc.buf_pos [buffer_cnt];
					buffer_len += desc.buf_pos [buffer_cnt];
				}
				file_pos *= sample_mul;
				file_pos += voice.spl.d2d.file_data_offset;
				buffer_len = MAX (buffer_len, 0);

				file_ptr = voice.spl.d2d.file_ptr;

				/* On se positionne dans le fichier */
				if (FILE_fseek (file_ptr, file_pos, SEEK_SET))
				{
					LOG_printf (
						"Warning: fseek() failed on D2D sample (voice %d).\n",
						voice_index
					);
					ret_val = -1;
				}

				else
				{
					/* On charge le buffer */
					if ((SLWORD) fread (buffer_ptr, sample_mul, buffer_len, file_ptr) != buffer_len)
					{
						LOG_printf (
							"Warning: couldn't load sample for D2D (voice_index %d).\n",
							voice_index
						);
						LOG_printf (
							"buffer_ptr = %p, mul = %d, len = %08X, file_ptr = %p\n",
							buffer_ptr,
							(int)sample_mul,
							(int)buffer_len,
							file_ptr
						);
						ret_val = -1;
					}

					else
					{
						desc.buf_flag [buffer_cnt] = true;
					}
				}
			}
		}
	}

	return (ret_val);
}



void	D2D_update_track (int track_index)
{
	Player &			player = Player::use_instance ();
	Player::TrackInfo	&	track_info =
		player._track_info [player._track_info_list [Pattern_TYPE_SPL] [track_index]];

	for (int voice_cnt = 0; voice_cnt < track_info.mix.spl.nbr_voices; ++voice_cnt)
	{
		const int		voice_index = track_info.mix.spl.voice_index_arr [voice_cnt];
		Voice &			voice = player._voice_arr [voice_index];

		D2D_update_d2d_cache_cell (voice);
	}
}



/*==========================================================================*/
/*      Nom: D2D_update_d2d_cache_cell                                      */
/*      Description: Mets a jour les positions des buffers dans le sample   */
/*                   en fonction de l'endroit de replay. Tient compte des   */
/*                   autres pistes qui partagent le buffer, et en cree un   */
/*                   nouveau si la desynchronisation est trop importante.   */
/*      Parametres en entree:                                               */
/*        - voice: Voix que l'on veut mettre a jour.                        */
/*==========================================================================*/

void	D2D_update_d2d_cache_cell (Voice &voice)
{
	int		buffer_cnt;
	int		buffer_to_keep_flags;
	int		first_time_offset;
	signed int	stop_direction;
	signed int	free_buffer;
	signed long	new_pos;
	long		buf_start [3];
	long		buf_end [3];
	long		stop_position;

	D2dBuffer &		desc = voice.spl.d2d._desc;

/*______________________________________________
 *
 * Regarde s'il y a eu des changements de
 * sample ou d'etat de D2D
 *______________________________________________
 */

	/* La piste n'est pas (ou plus) une piste de D2D */
	if (! voice.spl.d2d_flag)
	{
		if (desc.buffer_ptr [0] != NULL)
		{
			desc.deallocate ();
		}
		return;
	}

	/* La piste est en D2D, regarde s'il y a un descripteur */
	if (desc.buffer_ptr [0] != NULL)
	{
		/* Regarde si on a change de sample */
		if (voice.spl.d2d.file_ptr != desc.file_ptr)
		{
			desc.deallocate ();
		}
	}

	/* Y a pas de desc, on en cree un */
	if (desc.buffer_ptr [0] == NULL)
	{
		desc.allocate (voice);

		/* La creation n'a pas marche (plus de place),
		   on attendra le prochain coup pour reessayer. */
		if (desc.buffer_ptr [0] == NULL)
		{
			return;
		}
	}

/*______________________________________________
 *
 * Met a jour les informations
 *______________________________________________
 */

	long				current_pos   = voice.spl.get_playback_pos_int ();
	bool				back_flag     = voice.spl.is_running_backward ();
	const long		sample_length = voice.spl.reppos + voice.spl.replen;
	if (sample_length == 0)
	{
		return;
	}

	/* Le sample est fini ? */
	if (   current_pos >= sample_length
		 && voice.spl.loopmode != WaveForm_LOOP_TYPE_PP)
	{
		return;
	}

	buf_start [0] = 0;
	buf_end   [0] = voice.spl.d2d.startbuf_len;
	buf_start [1] = desc.buf_pos [0];
	buf_end   [1] = desc.buf_pos [0] + desc.buf_length [0];
	buf_start [2] = desc.buf_pos [1];
	buf_end   [2] = desc.buf_pos [1] + desc.buf_length [1];
	buffer_to_keep_flags = 1 << 0;

	/* Trouve la position sur laquelle on arretera le scan. Apres avoir franchi
	   cette position, on saura que toutes les donnees utiles sont a present en
		memoire. */

	/* -1: on tient compte uniquement de la position de stop pour s'arreter. */
	stop_direction = -1;

	/* Pas de bouclage */
	if (voice.spl.loopmode == WaveForm_LOOP_TYPE_NONE)
	{
		/* Vers l'avant */
		if (! back_flag)
		{
			/* On s'arrete a la fin du sample */
			stop_position = sample_length;
		}

		/* Vers l'arriere */
		else
		{
			/* On s'arrete quand on a atteint le debut du sample */
			stop_position = 0;
		}
	}

	/* Bouclage */
	else
	{
		/* Le point courant est dans la boucle */
		if (current_pos >= voice.spl.reppos)
		{
			/* Boucle normale et on va en arriere */
			if (back_flag && voice.spl.loopmode == WaveForm_LOOP_TYPE_FWD)
			{
				/* On s'arrete au debut du sample */
				stop_position = 0;
			}

			/* Sinon, on va vers l'avant ou en est en ping-pong loop */
			else
			{
				/* On s'arrete quand on est revenu au meme endroit,
				   dans la meme direction (pour le ping-pong loop). */
				stop_position = current_pos;
				stop_direction = int (back_flag);
			}
		}

		/* Le point courant est avant la boucle */
		else
		{
			/* A l'endroit: on s'arrete a la fin du sample */
			if (! back_flag)
			{
				stop_position = sample_length;
			}

			/* On va a l'envers: on s'arrete au debut */
			else
			{
				stop_position = 0;
			}
		}
	}

	/* Scanne le sample pour trouver si des parties utiles sont a charger en
	   memoire. On s'arrete quand tous les buffers sont remplis ou quand on a
	   atteint le point d'arret. */

	first_time_offset = 1;
	for ( ; ; )
	{
		free_buffer = -1;

		/* On repertorie tous les buffers qui sont deja en memoire
		   et qui nous seront utiles. */
		for (buffer_cnt = 0; buffer_cnt < 3; buffer_cnt ++)
		{
			/* On n'est pas dans ce buffer */
			if (   current_pos < buf_start [buffer_cnt]
				 || current_pos >= buf_end [buffer_cnt])
			{
				if ((buffer_to_keep_flags & (1 << buffer_cnt)) == 0)
				{
					/* Signale que ce buffer est libre, pour la partie dans
					   laquelle on devra charger un buffer. */
					free_buffer = buffer_cnt;
				}
				continue;
			}

			/* On essaie d'atteindre la fin du buffer */

			/* On va vers l'avant */
			if (! back_flag)
			{
				new_pos = buf_end [buffer_cnt];
				buffer_to_keep_flags |= 1 << buffer_cnt;

				/* On a depasse le point d'arret: on se tire */
				if (   stop_position >= current_pos + first_time_offset
				    && stop_position <= new_pos
				    && (   stop_direction == int (back_flag)
				        || stop_direction == -1))
				{
					buffer_to_keep_flags = 0x07;
					break;
				}

				current_pos = new_pos;
				buffer_cnt = -1;	/* On recommencera a scanner tous les buffers */

				/* Si on depasse le sample */
				if (current_pos >= sample_length)
				{
					if (voice.spl.loopmode == WaveForm_LOOP_TYPE_FWD)
					{
						current_pos = voice.spl.reppos;
					}

					/* Ping-pong loop */
					else if (voice.spl.loopmode == WaveForm_LOOP_TYPE_PP)
					{
						back_flag = true;
						current_pos = sample_length;
					}

					/* On sort du sample: ce cas ne devrait pas arriver */
					else
					{
						assert (false);
					}
				}
			}

			/* Vers l'arriere */
			else
			{
				new_pos = buf_start [buffer_cnt] - 1;
				buffer_to_keep_flags |= 1 << buffer_cnt;

				/* On a depasse le point d'arret: on se tire */
				if (   stop_position <= current_pos - first_time_offset
				    && stop_position >= new_pos
				    && (   stop_direction == int (back_flag)
				        || stop_direction == -1))
				{
					buffer_to_keep_flags = 0x07;
					break;
				}

				current_pos = new_pos;
				buffer_cnt = -1;	/* On recommencera a scanner tous les buffers */

				/* On sort de la boucle ping-pong */
				if (   current_pos <= voice.spl.reppos
				    && voice.spl.loopmode == WaveForm_LOOP_TYPE_PP)
				{
					back_flag = false;
					current_pos = voice.spl.reppos;
				}

				/* On sort du sample: ce cas ne devrait pas arriver */
				else if (current_pos <= 0)
				{
					assert (false);
				}
			}

			first_time_offset = 0;
		}

		/* On a fini de tout charger */
		if (buffer_to_keep_flags == 0x07 || free_buffer < 0)
		{
			break;
		}

		/* Remplit le buffer */
		if (! back_flag)
		{
			desc.buf_pos [free_buffer - 1] = current_pos;
			buf_start [free_buffer] = current_pos;
			buf_end [free_buffer] = current_pos + desc.buf_length [free_buffer - 1];
		}
		
		else
		{
			desc.buf_pos [free_buffer - 1] =   current_pos + 1
			                                      - desc.buf_length [free_buffer - 1];
			buf_start [free_buffer] = desc.buf_pos [free_buffer - 1];
			buf_end [free_buffer] = current_pos + 1;
		}

		desc.buf_flag [free_buffer - 1] = false;
		buffer_to_keep_flags |= 1 << free_buffer;
	}
}



/****************************************************************************/
/*                                                                          */
/*                                                                          */
/*                                                                          */
/****************************************************************************/



/*==========================================================================*/
/*      Nom:                                                                */
/*      Description:                                                        */
/*      Parametres en entree:                                               */
/*      Parametres en sortie:                                               */
/*      Parametres en entree/sortie:                                        */
/*      Retour:                                                             */
/*==========================================================================*/
