#include "PListRadio.h"

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#include "WimpLib:Exception.h"
#include "WimpLib:File.h"
#include "WimpLib:Keyboard.h"
#include "WimpLib:mem.h"
#include "WimpLib:Menu.h"
#include "WimpLib:Message.h"
#include "WimpLib:Task.h"
#include "WimpLib:Window.h"
#include "WimpLib:Utils.h"

#include "Cmds.h"
#include "DCDswi.h"
#include "DigitalCD.h"
#include "EditRadio.h"
#include "FileList.h"
#include "FLTrack.h"
#include "WimpLib:Hourglass.h"
#include "Media.h"
#include "Metadata.h"
#include "Options.h"
#include "Player.h"
#include "PlayList.h"
#include "RndList.h"
#include "Setup.h"
#include "UserEvents.h"

struct PListRadio
{
	PlayList        m_PlayList;
	const char*     m_pDriverInfo;
	const char*     m_pTitle;
	Media_LoopMode  m_LoopMode;
	FileList*       m_pEdit;
	FLTrack*        m_pRefTrack; // Current Track
	RndList*        m_pRndList;
};

static PListRadio* App_pPListRadio = NULL;

static void PListRadio_SetLoopMode(PlayList* pPlayList, Media_LoopMode mode);
static void PListRadio_OffsetTrackVolume(PListRadio*, int delta);

extern MediaDriver UrlDriver;

#define Icon_ShuffleMode 25
#define Icon_IntroScan   26
#define Icon_ListMode    27
#define Icon_SetTrack    28
#define Icon_DelX        35
#define Icon_DelFX       36
#define Icon_EditX       37
#define Icon_EditLX      38

#define Shuffle_Track_SortByNone    0x0000
#define Shuffle_Track_SortByRandom  0x0001
#define Shuffle_Track_SortByStation 0x0002
#define Shuffle_Track_SortByMask    0x0003
#define Shuffle_GroupBy_MimeType    0x0010
#define Shuffle_GroupBy_Broadcaster 0x0020
#define Shuffle_GroupBy_Bandwidth   0x0040
#define Shuffle_Random_Broadcaster  0x2000
#define Shuffle_DisableAll          0x80000000
#define Shuffle_Mask                0x80002073

static int Compare_Strings(const char* pa, const char* pb)
{
	if (pa)
	{
		if (*pa)
		{
			if (pb && *pb)
			{
				// First compare without taking case and accents into account
				int val = String_Collate(3, pa, pb, -1);

				// If equal, distinguish case and accents
				if (!val) val = String_Collate(0, pa, pb, -1);

				return val;
			}
			return 1;
		}
		else if (pb && !*pb)
			return 0;

		return -1;
	}
	else if (pb)
	{
		if (*pb)
			return -1;

		return 1;
	}

	return 0;
}

static int RadioPlayer_CheckCommand(void* handle, CmdID id)
{
	PListRadio* This = handle;
	Player* pPlayer = This->m_PlayList.m_pPlayer;
	int state = 0;

	if (pPlayer)
	{
		switch(id)
		{
			case Cmd_Ctrl_ToggleShuffle:
			{
				state = ECmdState_Allow;
				if (!(Player_GetShuffleMode(pPlayer) & Shuffle_DisableAll))
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_GroupBy_MimeType:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_GroupBy_MimeType) != 0)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_GroupBy_Broadcaster:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_GroupBy_Broadcaster) != 0)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_GroupBy_Rate:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_GroupBy_Bandwidth) != 0)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_SortBroadcaster_Random:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_Random_Broadcaster) != 0)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_SortBroadcaster_Name:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_Random_Broadcaster) == 0)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_SortBy_Random:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_Track_SortByMask) == Shuffle_Track_SortByRandom)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_SortBy_Name:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_Track_SortByMask) == Shuffle_Track_SortByStation)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_SortBy_None:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				state = ECmdState_Allow;
				if ((shuffle & Shuffle_Track_SortByMask) == Shuffle_Track_SortByNone)
					state |= ECmdState_Tick;
			}
			break;
			case Cmd_Ctrl_RestartSection:
			case Cmd_Ctrl_RestartTrack:
			case Cmd_Ctrl_Rewind:
			case Cmd_Ctrl_Forward:
			case Cmd_Ctrl_PrevSection:
			case Cmd_Ctrl_NextSection:
			case Cmd_Ctrl_ToggleProgram:
			case Cmd_Ctrl_ToggleCDProgram:
			{
				state = 0;
			}
			break;
			case Cmd_Ctrl_VolRel_FineUp:
			case Cmd_Ctrl_VolRel_FineDown:
			case Cmd_Ctrl_VolRel_Up:
			case Cmd_Ctrl_VolRel_Down:
			{
				state = ECmdState_Allow;
			}
			break;
			case Cmd_Ctrl_Track_Properties:
			case Cmd_Ctrl_Track_Show:
			{
				if (!Options()->Player.bParentalLock
				&&  (DCDUtils_GetFullScreenPlugIn() == -1)
				&&  (This->m_pRefTrack != NULL))
					state = ECmdState_Allow;
			}
			break;
			case Cmd_Ctrl_Track_Remove:
			{
				if (!Options()->Player.bParentalLock
				&&  (This->m_pRefTrack != NULL))
					state = ECmdState_Allow;
			}
			break;
			default:
				state = Player_CheckCommand(pPlayer, id);
		}
	}
	else
		state = App_CheckCommand(pPlayer, id);

	return state;
}

static bool RadioPlayer_ExecCommand(void* handle, CmdID id)
{
	PListRadio* This = handle;
	Player* pPlayer = This->m_PlayList.m_pPlayer;
	bool bRet = true;

	if (pPlayer)
	{
		switch(id)
		{
			case Cmd_Ctrl_ToggleShuffle:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				Player_SetShuffleMode(pPlayer, shuffle ^ Shuffle_DisableAll);
			}
			break;
			case Cmd_Ctrl_GroupBy_MimeType:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				Player_SetShuffleMode(pPlayer, shuffle ^ Shuffle_GroupBy_MimeType);
			}
			break;
			case Cmd_Ctrl_GroupBy_Broadcaster:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				Player_SetShuffleMode(pPlayer, shuffle ^ Shuffle_GroupBy_Broadcaster);
			}
			break;
			case Cmd_Ctrl_GroupBy_Rate:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				Player_SetShuffleMode(pPlayer, shuffle ^ Shuffle_GroupBy_Bandwidth);
			}
			break;
			case Cmd_Ctrl_SortBroadcaster_Random:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				Player_SetShuffleMode(pPlayer, shuffle | Shuffle_Random_Broadcaster);
			}
			break;
			case Cmd_Ctrl_SortBroadcaster_Name:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				Player_SetShuffleMode(pPlayer, shuffle & ~Shuffle_Random_Broadcaster);
			}
			break;
			case Cmd_Ctrl_SortBy_Random:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				shuffle &= ~Shuffle_Track_SortByMask;
				Player_SetShuffleMode(pPlayer, shuffle | Shuffle_Track_SortByRandom);
			}
			break;
			case Cmd_Ctrl_SortBy_Name:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				shuffle &= ~Shuffle_Track_SortByMask;
				Player_SetShuffleMode(pPlayer, shuffle | Shuffle_Track_SortByStation);
			}
			break;
			case Cmd_Ctrl_SortBy_None:
			{
				unsigned int shuffle = Player_GetShuffleMode(pPlayer);

				shuffle &= ~Shuffle_Track_SortByMask;
				Player_SetShuffleMode(pPlayer, shuffle | Shuffle_Track_SortByNone);
			}
			break;
			case Cmd_Ctrl_VolRel_FineUp:
			{
				PListRadio_OffsetTrackVolume(This, 5);
			}
			break;
			case Cmd_Ctrl_VolRel_FineDown:
			{
				PListRadio_OffsetTrackVolume(This, -5);
			}
			break;
			case Cmd_Ctrl_VolRel_Up:
			{
				PListRadio_OffsetTrackVolume(This, 20);
			}
			break;
			case Cmd_Ctrl_VolRel_Down:
			{
				PListRadio_OffsetTrackVolume(This, -20);
			}
			break;
			case Cmd_Ctrl_Track_Properties:
			{
				if (DCDUtils_GetFullScreenPlugIn() == -1)
				{
					List* plist = New_List();

					List_InsertBefore(plist, NULL, This->m_pRefTrack);
					EditRadio_Edit(NULL, plist, This->m_pRefTrack);

					Delete_List(plist);
				}
			}
			break;
			case Cmd_Ctrl_Track_Show:
			{
				if (DCDUtils_GetFullScreenPlugIn() == -1)
				{
					FLTrack* pTrack = This->m_pRefTrack;

					if (pTrack)
					{
						FileList* pOwner = FLTrack_GetOwner(pTrack);

						if (pOwner)
						{
							FileList_Show(pOwner, -1);
							FileList_Show(pOwner, FileList_FindTrack(pOwner, pTrack));
						}
					}
				}
			}
			break;
			case Cmd_Ctrl_Track_Remove:
			{
				FLTrack* pTrack = This->m_pRefTrack;

				if (pTrack)
				{
					if (Options()->Player.bRemoveAsNeverPlay)
					{
						FLTrack_SetFlags(pTrack, FLTrack_NeverPlay, FLTrack_NeverPlay);
						FLTrack_RefreshViews(pTrack, This, true, false);
						Player_ShowMessage(pPlayer, Msg_Lookup("CmdNeverPlay"));
					}
					else
					{
						FileList* pOwner = FLTrack_GetOwner(pTrack);

						if (pOwner)
						{
							FileList_StartUpdate(pOwner);
							FileList_DelTrack(pOwner, pTrack);
							FileList_EndUpdate(pOwner, false, true);
						}
					}
				}
			}
			break;
			default:
				bRet = Player_ExecCommand(pPlayer, id);
		}
	}
	else
		bRet = App_ExecCommand(pPlayer, id);

	return bRet;
}

static const CmdHandler RadioPlayer_CmdHandler =
{ RadioPlayer_CheckCommand
, RadioPlayer_ExecCommand
};

static void RadioPlayer_RefreshIcons(Player* pPlayer)
{
	PListRadio* This = (PListRadio*) Player_GetPlayList(pPlayer);
	bool b;

	b = ((RadioPlayer_CheckCommand(This, Cmd_Ctrl_Track_Remove) & ECmdState_Allow) != 0);
	Icon_SetHighlight(Player_GetWindow(pPlayer, 0), Icon_DelX, b);
	Icon_SetHighlight(Player_GetWindow(pPlayer, 1), Icon_DelX, b);

	Icon_SetHighlight(Player_GetWindow(pPlayer, 0), Icon_DelFX, false);
	Icon_SetHighlight(Player_GetWindow(pPlayer, 1), Icon_DelFX, false);

	b = ((RadioPlayer_CheckCommand(This, Cmd_Ctrl_Track_Properties) & ECmdState_Allow) != 0);
	Icon_SetHighlight(Player_GetWindow(pPlayer, 0), Icon_EditX, b);
	Icon_SetHighlight(Player_GetWindow(pPlayer, 1), Icon_EditX, b);

	b = ((RadioPlayer_CheckCommand(This, Cmd_App_ShowMainList) & ECmdState_Allow) != 0);
	Icon_SetHighlight(Player_GetWindow(pPlayer, 0), Icon_EditLX, b);
	Icon_SetHighlight(Player_GetWindow(pPlayer, 1), Icon_EditLX, b);
}

static EListenerAction RadioPlayer_EventHandler(void* handle, const Event* e)
{
	Player* pPlayer = handle;
	PListRadio* This = (PListRadio*) Player_GetPlayList(pPlayer);

	switch(e->Type)
	{
		case EEvent_Null:
		{
			// Could be due to plug-ins
			RadioPlayer_RefreshIcons(pPlayer);
		}
		break;
		case EEvent_Mouse:
		{
			const Mouse* m = e->pData;

			if (m->but & EBut_Menu)
			{
				if  (m->i != Icon_SetTrack)
				{
					CmdHandler_OpenMenu(&RadioPlayer_CmdHandler, This, "Ctrl_Radio");

					return EListenerAction_StopEvent;
				}
				return EListenerAction_ContinueEvent;
			}

			switch(m->i)
			{
				case Icon_ShuffleMode:
				{
					if (m->but == EBut_Adjust)
					{
						unsigned int shuffle = Player_GetShuffleMode(pPlayer);

						Player_SetShuffleMode(pPlayer, shuffle ^ Shuffle_DisableAll);
					}
					else
					{
						CmdHandler_OpenMenu(&RadioPlayer_CmdHandler, This, "Ctrl_Radio_Shuffle");
					}

					return EListenerAction_StopEvent;
				}
				break;
				case Icon_IntroScan:
				{
					return EListenerAction_StopEvent;
				}
				break;
				case Icon_DelX:
				{
					CmdHandler_ExecCommand(&RadioPlayer_CmdHandler, This, Cmd_Ctrl_Track_Remove);
					return EListenerAction_StopEvent;
				}
				break;
				case Icon_EditX:
				{
					if (m->but == EBut_Adjust)
						CmdHandler_ExecCommand(&RadioPlayer_CmdHandler, This, Cmd_Ctrl_Track_Show);
					else
						CmdHandler_ExecCommand(&RadioPlayer_CmdHandler, This, Cmd_Ctrl_Track_Properties);
					return EListenerAction_StopEvent;
				}
				break;
				case Icon_EditLX:
				{
					CmdHandler_ExecCommand(&RadioPlayer_CmdHandler, This, Cmd_App_ShowMainList);
					return EListenerAction_StopEvent;
				}
				break;
			}

			return EListenerAction_ContinueEvent;
		}
		break;
		case EEvent_Key:
		{
			const Event_Key* key = e->pData;

			// Ignore key presses if shortcuts are disabled
			// unless a full screen plug-in forwards them to use
			if (!Player_AllowsShortcuts(pPlayer)
			&&  (DCDUtils_GetFullScreenPlugIn() == -1))
				return EListenerAction_ContinueEvent;

			if (CmdHandler_KeyPressed(&RadioPlayer_CmdHandler, This, "Ctrl_Radio", key->code))
				return EListenerAction_StopEvent;

			return EListenerAction_ContinueEvent;
		}
		break;
		case EEvent_Message:
		case EEvent_MessageWantAck:
		{
			const Msg_HelpRequest* msg = e->pData;

			switch(msg->hdr.action)
			{
				case EMsg_HelpRequest:
				{
					const char* str;

					if (msg->m.i == Icon_DelX)
						str = Msg_Lookup("CtDSH001");
					else if (msg->m.i == Icon_DelFX)
						str = Msg_Lookup("CtDSH002");
					else if (msg->m.i == Icon_EditX)
						str = Msg_Lookup("CtDSH003");
					else if (msg->m.i == Icon_EditLX)
						str = Msg_Lookup("CtDSH004");
					else if (msg->m.i == Icon_ListMode)
						str = Msg_Lookup("CtDSH005");
					else return EListenerAction_ContinueEvent;

					Task_HelpReply(msg, str);
					return EListenerAction_StopEvent;
				}
				break;
			}
		}
		break;
	}

	return EListenerAction_ContinueEvent;
}

static void RadioPlayer_ReloadSkin(Player* pPlayer)
{
	try
	{
		throw_Player_ReloadSkin(pPlayer);
		throw_Window_RegisterEventHandler(Player_GetWindow(pPlayer, 0), RadioPlayer_EventHandler, pPlayer, true);
		throw_Window_RegisterEventHandler(Player_GetWindow(pPlayer, 1), RadioPlayer_EventHandler, pPlayer, true);

		RadioPlayer_RefreshIcons(pPlayer);
	}
	catch
	{
		App_ReportException();
	}
	catch_end
}

static Player* throw_PListRadio_CreatePlayer(PlayList* pPlayList)
{
	Player* pPlayer = throw_New_Player(pPlayList, "", (FPlayer_ReloadSkin) RadioPlayer_ReloadSkin
									, Player_ShuffleMenu | Player_RememberPosition);

	try
	{
		Player_SetCmdHandler(pPlayer, &RadioPlayer_CmdHandler, pPlayList);
		throw_Window_RegisterEventHandler(Player_GetWindow(pPlayer, 0), RadioPlayer_EventHandler, pPlayer, true);
		throw_Window_RegisterEventHandler(Player_GetWindow(pPlayer, 1), RadioPlayer_EventHandler, pPlayer, true);
		throw_Task_AddListener(EEvent_Null, RadioPlayer_EventHandler, pPlayer, false);

		pPlayList->m_pPlayer = pPlayer;

		Player_SetShuffleMode(pPlayer, Player_GetShuffleMode(pPlayer) & Shuffle_Mask);
		Player_SetIntroScan(pPlayer, false);
		Player_Refresh(pPlayer, player_refresh_tracklist);
		RadioPlayer_RefreshIcons(pPlayer);
	}
	catch
	{
		pPlayList->m_pPlayer = NULL;
		Delete_Player(pPlayer);
		throw_current();
	}
	catch_end

	return pPlayer;
}

static void PListRadio_DeletePlayer(PlayList* pPlayList)
{
	Player* pPlayer = pPlayList->m_pPlayer;

	if (!pPlayer) return;

	Task_RemoveListener(EEvent_Null, RadioPlayer_EventHandler, pPlayer);
	Window_DeRegisterEventHandler(Player_GetWindow(pPlayer, 0), RadioPlayer_EventHandler, pPlayer);
	Window_DeRegisterEventHandler(Player_GetWindow(pPlayer, 1), RadioPlayer_EventHandler, pPlayer);
	Delete_Player(pPlayer);
	pPlayList->m_pPlayer = NULL;
}

/*-------------------------------------------------------------------------------*/

static void PListRadio_StopCurrentTrack(PListRadio* This, bool bInError)
{
	FLTrack* pRefTrack = This->m_pRefTrack;
	bool bMod = false;

	if (!pRefTrack) return;

	This->m_pRefTrack = NULL;

	// Stop playing, Unload file
	if (This->m_PlayList.m_pObject)
	{
		Media_Stop(This->m_PlayList.m_pObject);
		Media_UnLoadObject(This->m_PlayList.m_pObject);
		This->m_PlayList.m_pObject = NULL;
	}

	if (bInError)
		bMod |= FLTrack_SetFlags(pRefTrack, FLTrack_FailedToPlay, FLTrack_FailedToPlay);

	// Remove playing mark in playlist
	FLTrack_RefreshViews(pRefTrack, This, bMod, false);

	// Remove driver info
	mem_free(This->m_pDriverInfo);
	This->m_pDriverInfo = NULL;
}

static void PListRadio_OffsetTrackVolume(PListRadio* This, int delta)
{
	if (This->m_pRefTrack)
	{
		FLTrack_SetVolume(This->m_pRefTrack, FLTrack_GetVolume(This->m_pRefTrack) + delta);
		FLTrack_RefreshViews(This->m_pRefTrack, This, true, false);
		PListRadio_RefreshPlayer(This, player_refresh_volume);
		if (This->m_PlayList.m_pPlayer)
			Player_ShowMessage(This->m_PlayList.m_pPlayer
			                 , SPrintf(Msg_Lookup("CmdRelVolume:")
			                 , FLTrack_GetVolume(This->m_pRefTrack)/1.));
	}
}

/*-------------------------------------------------------------------------------*/

static bool PListRadio_Init(PlayList* pPlayList, bool identify)
{
	PListRadio* This = (PListRadio*) pPlayList;
	const char* title;

	IGNORE(identify);

	// clear all texts
	if (This->m_PlayList.nr_tracks <= 0)
		title = Msg_Lookup("NoRadios");
	else
		title = Msg_Lookup("UnkRadios");
	mem_setstring(&This->m_pTitle, title);

	return false;
}

// Order of enum must follow sorting order
typedef enum
{ Entity_Unknown             = 0x0
, Entity_RD_MimeType         = 0x1
, Entity_RD_Bandwidth        = 0x2
, Entity_RD_Broadcaster      = 0x3
, Entity_None                = 0xf
} Entity;

static unsigned int GetBand(unsigned int rate)
{
	int i;

	for (i = 0; i < 4; i++)
	{
		if (rate < Options()->Internet.BandRates[i])
			break;
	}

	return i;
}

static Entity Track_GetSubEntity(const FLTrack* pTrack, unsigned int shuffle, Entity e, const char** pData)
{
	switch (e)
	{
		case Entity_Unknown:
		{
			*pData = FLTrack_GetMetaString(pTrack, EMetaId_StreamMimeType);
			if ((shuffle & Shuffle_GroupBy_MimeType) && **pData)
				return Entity_RD_MimeType;
		}
        // no break;
        case Entity_RD_MimeType:
		{
			if (shuffle & Shuffle_GroupBy_Bandwidth)
				return Entity_RD_Bandwidth;
		}
		// no break;
        case Entity_RD_Bandwidth:
		{
			*pData = FLTrack_GetMetaString(pTrack, EMetaId_StreamBroadcaster);
			if ((shuffle & Shuffle_GroupBy_Broadcaster) && **pData)
				return Entity_RD_Broadcaster;
		}
		break;
	}

	return Entity_None;
}

static int Compare_Tracks(PListRadio* This, unsigned int shuffle, const FLTrack* pTrackA, const FLTrack* pTrackB)
{
	const char* pDataA;
	const char* pDataB;
	int val;
	Entity entityA = Entity_Unknown;
	Entity entityB = Entity_Unknown;

	while(1)
	{
		entityA = Track_GetSubEntity(pTrackA, shuffle, entityA, &pDataA);
		entityB = Track_GetSubEntity(pTrackB, shuffle, entityB, &pDataB);

		// Entity compare
		val = entityA - entityB;
		if (val) return val;

		// They belong to the same kind of entity

		// Terminal entities?
		if (entityA >= Entity_None)
			break;

		if (entityA == Entity_RD_Bandwidth)
		{
			val = GetBand(FLTrack_GetRate(pTrackA))
			    - GetBand(FLTrack_GetRate(pTrackB));
			if (val) return val;
		}
		else
		{
			// Ordering/shuffling of non-terminal entities
			val = RndList_GetOrder(This->m_pRndList, pDataA)
	    		- RndList_GetOrder(This->m_pRndList, pDataB);
			if (val) return val;
		}
	}

	// Ordering/shuffling of terminal entities (tracks)
	switch(shuffle & Shuffle_Track_SortByMask)
	{
		case Shuffle_Track_SortByStation:
		{
			if (!val) val = Compare_Strings(FLTrack_GetTrackName(pTrackA), FLTrack_GetTrackName(pTrackB));
		}
		break;
		default: // SortByRandom, SortByNone
		{
			val = RndList_GetOrder(This->m_pRndList, pTrackA)
			    - RndList_GetOrder(This->m_pRndList, pTrackB);
		}
		break;
	}

	// Must somehow distinguish them
	if (!val) val = ((char*) pTrackA) - ((char*) pTrackB);

	return val;
}

static void QSort_Tracks(PListRadio* This, unsigned int shuffle, const void** a, int n)
{
	// insertion sort on small arrays
	if (n < 7)
	{
		const void** pl;
		const void** pm;
		const void** pn = a + n;
		const void* pt;

		for (pm = a + 1; pm < pn; pm++)
		{
			for (pl = pm; (pl > a) && (Compare_Tracks(This, shuffle, pl[-1], pl[0]) > 0); pl--)
			{
				pt = pl[0];
				pl[0] = pl[-1];
				pl[-1] = pt;
			}
		}

		return;
	}

	const void* pv = a[n/2];
	const void* pt;

	int i = 0, j = n;

	a[n/2] = a[0];
	a[0] = pv;

	for (;;)
	{
		do i++; while ((i < j) && (Compare_Tracks(This, shuffle, a[i], pv) < 0));
		do j--; while ((i <= j) && (Compare_Tracks(This, shuffle, pv, a[j]) < 0));
		if (j < i) break;
		pt = a[i];
		a[i] = a[j];
		a[j] = pt;
	}
	pt = a[0];
	a[0] = a[j];
	a[j] = pt;
	QSort_Tracks(This, shuffle, a, j);
	QSort_Tracks(This, shuffle, a + j + 1, n - j - 1);
}

static int PListRadio_GetTrackList(PlayList* pPlayList, const void*** pptracks, bool list, unsigned int shuffle)
{
	PListRadio* This = (PListRadio*) pPlayList;
	const void** ptracks;

	IGNORE(list);

	This->m_PlayList.nr_tracks = 0;

	if (This->m_pRndList)
	{
		Delete_RndList(This->m_pRndList);
		This->m_pRndList = NULL;
	}

	if (This->m_pEdit)
		This->m_PlayList.nr_tracks += FileList_CountTracksToPlay(This->m_pEdit, FLTrack_Type_Url);

	if (This->m_PlayList.nr_tracks <= 0)
	{
		*pptracks = mem_alloc(sizeof(void*));
		if (!*pptracks) App_ReportError("NoMem");
		return 0;
	}

	*pptracks = ptracks = mem_alloc(This->m_PlayList.nr_tracks * sizeof(void*));
	if (!ptracks)
	{
		This->m_PlayList.nr_tracks = 0;
		App_ReportError("NoMem");
		return 0;
	}

	if (This->m_pEdit)
		ptracks = FileList_ListTracksToPlay(This->m_pEdit, ptracks, FLTrack_Type_Url);

	throw_assert(*pptracks + This->m_PlayList.nr_tracks == ptracks);

	ptracks = *pptracks;

	if (shuffle)
	{
		try
		{
			This->m_pRndList = throw_New_RndList();
			// Determine an MIME types order
			throw_RndList_AddSet(This->m_pRndList, (const OrderedSet*) DigitalCD.pMimeTypesCol, 0);
			// Broadcaster follows
			throw_RndList_AddSet(This->m_pRndList, (const OrderedSet*) DigitalCD.pBroadcastersCol, (shuffle & Shuffle_Random_Broadcaster) != 0);
			// Tracks follows
			throw_RndList_AddArray
				( This->m_pRndList
				, ptracks
				, This->m_PlayList.nr_tracks
				, (shuffle & Shuffle_Track_SortByMask) == Shuffle_Track_SortByRandom
				);
			QSort_Tracks(This, shuffle, ptracks, This->m_PlayList.nr_tracks);
		}
		catch
		{
			// The list just remains unsorted
		}
		catch_end
	}

	return This->m_PlayList.nr_tracks;
}

static const char* PListRadio_GetText(PlayList* pPlayList, void* ptrack, EMetaId id)
{
	PListRadio* This = (PListRadio*) pPlayList;
	FLTrack* pTrack = ptrack;
	const char* text = NULL;

	if (pTrack == NULL)
	{
		switch(id)
		{
			case EMetaId_StreamType:
			{
				text = This->m_pTitle;
			}
			break;
		}
	}
	else if (id == EMetaId_StreamName)
		text = FLTrack_GetTrackName(pTrack);
	else
	{
		const MetaData* meta;

		// Playing info may be richer than static info
		if (pPlayList->m_pObject && (This->m_pRefTrack == pTrack))
		{
			meta = MediaObject_FindMeta(pPlayList->m_pObject, id);

			if (meta && meta->data)
				text = meta->data;
		}

		if (!text)
		{
			meta = MetaList_Find(FLTrack_GetMeta(pTrack), id);

			if (meta && meta->data)
				text = meta->data;
		}
	}

	if (!text) text = &nil;

	return text;
}

static bool PListRadio_PlayTrack(PlayList* pPlayList, void* ptrack)
{
	PListRadio* This = (PListRadio*) pPlayList;
	Player* pPlayer = This->m_PlayList.m_pPlayer;
	FLTrack* pTrack = ptrack;
	const char* urlname = NULL;
	uint64_t startpos;
	bool bMod = false;

	if (!pPlayer) return false;

	//
	// Step 1: Stop currently playing track
	//

	PListRadio_StopCurrentTrack(This, false);

	//
	// Step 2: Load new track, extract info and play it
	//

	try
	{
		// Build urlname
		throw_mem_setstring(&urlname, FLTrack_GetFilename(pTrack));

		throw_os(Media_LoadObject(&pPlayList->m_pObject, urlname, 0, &UrlDriver));
	}
	catch
	{
		App_DescribeError(&exception_current()->error, "RDErr0:%s", urlname);
	}
	catch_end

	mem_free(urlname);

	if (pPlayList->m_pObject == NULL)
	{
		if (FLTrack_SetFlags(pTrack, FLTrack_FailedToPlay, FLTrack_FailedToPlay))
			FLTrack_RefreshViews(pTrack, This, true, false);

		RadioPlayer_RefreshIcons(pPlayer);

		return false;
	}

	startpos = Media_GetSectionPos(pPlayList->m_pObject, FLTrack_GetSection(pTrack));

	const MetaData* meta = MediaObject_FindMeta(pPlayList->m_pObject, EMetaId_StreamType);
	mem_setstring(&This->m_pDriverInfo, (meta && meta->data) ? meta->data : NULL);

	Player_SetHardwareType(pPlayer, PlayList_GetHardwareType(pPlayList));

	// New track is ok
	This->m_pRefTrack = pTrack;

	// Setup playing parameters (cannot be done before m_pRefTrack is set) and start playing
	bMod = FLTrack_SetFlags(pTrack, FLTrack_Played | FLTrack_PlayedToday
	                              , FLTrack_Played | FLTrack_PlayedToday | FLTrack_Invalidated | FLTrack_FailedToPlay);
	// Force refresh to at least show play mark
	FLTrack_RefreshViews(pTrack, This, bMod, false);

	PListRadio_RefreshPlayer(This, player_refresh_track | player_refresh_volume);

	PListRadio_SetLoopMode(pPlayList, This->m_LoopMode);

	Media_SetConfig(pPlayList->m_pObject, Media_CfgFrequency, &Options()->Music_Files.Frequency);
	Media_SetConfig(pPlayList->m_pObject, Media_CfgInterpol, &Options()->Music_Files.bInterpol);
	Media_SetConfig(pPlayList->m_pObject, Media_CfgBalance, &Options()->Music_Files.Balance);
	Media_SetConfig(pPlayList->m_pObject, Media_CfgStereoSeparation, &Options()->Music_Files.StereoSeparation);
	Media_SetConfig(pPlayList->m_pObject, Media_CfgVolumeRamping, &Options()->Music_Files.bVolumeRamping);
	Media_SetConfig(pPlayList->m_pObject, Media_CfgEqualizer, Options());

	Media_Play(pPlayList->m_pObject, startpos);
	Media_SetConfig(pPlayList->m_pObject, Media_CfgDMASize, &Options()->Music_Files.DMASize);

	RadioPlayer_RefreshIcons(pPlayer);

	return true;
}

static Media_Status PListRadio_GetStatus(PlayList* pPlayList)
{
	PListRadio* This = (PListRadio*) pPlayList;

	if (This->m_PlayList.nr_tracks <= 0) return status_empty;
	if (pPlayList->m_pObject == NULL) return status_stop;

	return Media_GetStatus(pPlayList->m_pObject);
}

static void PListRadio_SetLoopMode(PlayList* pPlayList, Media_LoopMode mode)
{
	PListRadio* This = (PListRadio*) pPlayList;

	This->m_LoopMode = mode;

	if (pPlayList->m_pObject)
	{
		// List with 1 track, translate loop on list to avoid reloads
		if ((mode == loop_volume_loop) && (This->m_PlayList.nr_tracks == 1))
			mode = loop_section_loop;

		// Translate into loop mode at url level
		if (mode != loop_section_loop)
			mode = loop_section_play;

		// Translate back into volume level if play all sections is present
		if (FLTrack_GetFlags(This->m_pRefTrack) & FLTrack_PlayAllSections)
		{
			if (mode == loop_section_loop)
				mode = loop_volume_loop;
			else
				mode = loop_volume_play;
		}

		Media_SetLoopMode(pPlayList->m_pObject, mode);
	}
}

static void PListRadio_Play(PlayList* pPlayList, uint64_t current)
{
	PListRadio* This = (PListRadio*) pPlayList;

	PListRadio_SetLoopMode(pPlayList, This->m_LoopMode);
	Media_Play(pPlayList->m_pObject, current);
	Media_SetConfig(pPlayList->m_pObject, Media_CfgDMASize, &Options()->Music_Files.DMASize);
}

static void PListRadio_Stop(PlayList* pPlayList, bool bInError)
{
	PListRadio* This = (PListRadio*) pPlayList;
	Player* pPlayer = This->m_PlayList.m_pPlayer;

	if (!pPlayer) return;

	PListRadio_StopCurrentTrack(This, bInError);

	RadioPlayer_RefreshIcons(pPlayer);
}

static void PListRadio_GetPosition(PlayList* pPlayList, void* pTrack, MediaPosition* newpos)
{
	PListRadio* This = (PListRadio*) pPlayList;
	MediaPosition* pos = &pPlayList->m_Position;

	if (pPlayList->m_pObject == NULL)
		memset(newpos, 0, sizeof(*newpos));
	else if (This->m_pRefTrack == pTrack)
	{
		Media_GetPosition(pPlayList->m_pObject, newpos);
		if (pos->section.nr != newpos->section.nr)
			pPlayList->m_pObject->m_Updates |= Media_Updated_Length;

		if (FLTrack_GetFlags(pTrack) & FLTrack_PlayAllSections)
		{
			newpos->section.pos = newpos->volume.pos;
			newpos->section.len = newpos->volume.len;
		}
	}

	pPlayList->m_Position = *newpos;
}

static int PListRadio_GetTrackNummer(PlayList* pPlayList, void* ptrack)
{
	IGNORE(pPlayList);
	IGNORE(ptrack);

	return 0; // Use the player track index
}

static int PListRadio_GetModuleHandle(PlayList* pPlayList, void* ptrack)
{
	PListRadio* This = (PListRadio*) pPlayList;
	FLTrack* pTrack = ptrack;

	if (pTrack == NULL)
		return 0;
	else
	{
		const MetaData* meta;

		// Playing info may be richer than static info
		if (pPlayList->m_pObject && (This->m_pRefTrack == pTrack))
		{
			meta = MediaObject_FindMeta(pPlayList->m_pObject, EMetaId_ModuleHandle);

			if (meta)
				return (int) meta->data;
		}
	}

	return 0;
}

#define MaxVolume (100<<16)

static void PListRadio_SetConfig(PlayList* pPlayList, Media_Config type, const void* value)
{
	PListRadio* This = (PListRadio*) pPlayList;
	Player* pPlayer = This->m_PlayList.m_pPlayer;

	if (!pPlayer) return;

	switch(type)
	{
		case Media_CfgVolume:
		{
			MediaVolume* pvol = (MediaVolume*) value;
			unsigned int val;
			unsigned int mainval = pvol->sysscaled;

			if (This->m_pRefTrack)
			{
				pvol->raw = ((pvol->raw/100) * FLTrack_GetVolume(This->m_pRefTrack)) / 100;
				pvol->sysscaled = ((pvol->sysscaled/100) * FLTrack_GetVolume(This->m_pRefTrack)) / 100;
				Media_SetConfig(pPlayList->m_pObject, Media_CfgVolume, pvol);
				if (Options()->PlugIns.bRescaleWithTrackVolume == false)
					val = mainval;
				else
					val= pvol->sysscaled * 100;

				// Fix value for DCDUtils
				if (val > MaxVolume) val = MaxVolume;
			}
			else
			{
				val = mainval = MaxVolume;
			}

			DCDUtils_SetIntInfo(Player_GetDCDUtilsHandle(pPlayer), DCD_Info_MainVolume, mainval >> 8);
			DCDUtils_SetIntInfo(Player_GetDCDUtilsHandle(pPlayer), DCD_Info_ScaleVolume, val >> 8);
		}
		break;
		default:
			if (This->m_pRefTrack)
			{
				Media_SetConfig(pPlayList->m_pObject, type, value);
			}
	}
}

static unsigned int PListRadio_GetConfig(PlayList* pPlayList, Media_Config type)
{
	switch(type)
	{
		case PlayList_CfgAutoStart:
		{
			return false;
		}
		break;
		case PlayList_CfgSeekable:
		{
			if (pPlayList->m_pObject)
				return Media_GetSeekable(pPlayList->m_pObject);

			return false;
		}
		break;
		case Media_CfgVolume:
		{
			return pPlayList->m_Volume;
		}
		break;
	}

	return 0xffffffff;
}

static unsigned int PListRadio_GetUpdates(PlayList* pPlayList)
{
	PListRadio* This = (PListRadio*) pPlayList;
	FLTrack* pTrack = This->m_pRefTrack;
	unsigned int updates = 0;
	bool bModified = false;
	const MetaData* meta;

	if (pPlayList->m_pObject == NULL) return 0;

	updates = Media_GetUpdates(pPlayList->m_pObject);

	if (updates & Media_Updated_Meta)
	{
		meta = MediaObject_FindMeta(pPlayList->m_pObject, EMetaId_StreamStation);
		if (meta && meta->data)
			bModified |= FLTrack_SetMetaText(pTrack, EMetaId_StreamStation, meta->origin, meta->data);

		if (bModified)
			FLTrack_RefreshViews(pTrack, This, true, false);
	}

	// Simulate new song when title changes
	if (updates & Media_Updated_Title)
		updates |= Media_New_Song;

	return updates;
}

static EListenerAction PListRadio_Listener(void* handle, const Event* e)
{
	PListRadio* This = handle;
	Player* pPlayer = This->m_PlayList.m_pPlayer;

	switch(e->Type)
	{
		case EEvent_OptionMixingParameters:
		{
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgFrequency, &Options()->Music_Files.Frequency);
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgInterpol, &Options()->Music_Files.bInterpol);
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgVolumeRamping, &Options()->Music_Files.bVolumeRamping);
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgBalance, &Options()->Music_Files.Balance);
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgStereoSeparation, &Options()->Music_Files.StereoSeparation);
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgEqualizer, Options());
		}
		break;
		case EEvent_OptionMixingFrequency:
		{
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgFrequency, e->pData);
		}
		break;
		case EEvent_OptionMixingInterpol:
		{
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgInterpol, e->pData);
		}
		break;
		case EEvent_OptionMixingVolRamping:
		{
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgVolumeRamping, e->pData);
		}
		break;
		case EEvent_OptionMixingBalance:
		{
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgBalance, e->pData);
		}
		break;
		case EEvent_OptionMixingStereo:
		{
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgStereoSeparation, e->pData);
		}
		break;
		case EEvent_OptionMixingEqualizer:
		{
			PListRadio_SetConfig(&This->m_PlayList, Media_CfgEqualizer, e->pData);
		}
		break;
		case EEvent_OptionParentalLock:
		{
			if (pPlayer) RadioPlayer_RefreshIcons(pPlayer);
		}
		break;
		case EEvent_OptionRescaleVolume:
		{
			PListRadio_RefreshPlayer(This, player_refresh_volume);
		}
		break;
	}

	return EListenerAction_ContinueEvent;
}

static EJobAction Job_New_PListRadio_RefreshTrackList(void* pOwner, EJobAction action, void* pParams)
{
	PListRadio* This = pOwner;
	IGNORE(pParams);

	try
	{
		if (action == EJobAction_Continue)
		{
			Player* pPlayer = This->m_PlayList.m_pPlayer;
			MediaObject* pObject = This->m_PlayList.m_pObject;
			bool off = !WLib_Hourglass_IsOn();

			if (off) WLib_Hourglass_On();
			PListRadio_Init(&This->m_PlayList, false);

			if (pObject)
			{
				const MetaData* meta = MediaObject_FindMeta(pObject, EMetaId_StreamType);
				mem_setstring(&This->m_pDriverInfo, (meta && meta->data) ? meta->data : NULL);
			}

			if (pPlayer && This->m_pEdit) Player_Refresh(pPlayer, player_refresh_tracklist | player_refresh_volume);

			PListRadio_RefreshPlayer(This, player_refresh_track | player_refresh_volume);
			if (off) WLib_Hourglass_Off();
		}

		action = EJobAction_Stop;
	}
	catch
	{
		App_ReportException();
		action = EJobAction_Stop;
	}
	catch_end

	return action;
}

void PListRadio_RefreshTrackList(PListRadio* This)
{
	JobList_Add(Task_GetJobList(), true, This, NULL, Job_New_PListRadio_RefreshTrackList);
}

/*-------------------------------------------------------------------------------*/

static const PlayList_Cmds PListRadio_Cmds =
{
	  PListRadio_Init
	, PListRadio_GetTrackList
	, PListRadio_GetText
	, NULL
	, PListRadio_PlayTrack
	, PListRadio_GetStatus
	, PListRadio_SetLoopMode
	, PListRadio_Play
	, PListRadio_Stop
	, PListRadio_GetPosition
	, PListRadio_GetTrackNummer
	, PListRadio_GetModuleHandle
	, PListRadio_SetConfig
	, PListRadio_GetConfig
	, PListRadio_GetUpdates
	, throw_PListRadio_CreatePlayer
	, PListRadio_DeletePlayer
};

static void Delete_PListRadio(PListRadio* This)
{
	if (This)
	{
		Task_RemoveListener(EEvent_OptionMixingParameters, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionMixingFrequency, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionMixingInterpol, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionMixingVolRamping, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionMixingBalance, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionMixingStereo, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionMixingEqualizer, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionParentalLock, PListRadio_Listener, This);
		Task_RemoveListener(EEvent_OptionRescaleVolume, PListRadio_Listener, This);

		PlayList_NotPlayList(&This->m_PlayList);

		mem_free(This->m_pTitle);
		mem_free(This->m_pDriverInfo);

		mem_free(This);
	}
}

static PListRadio* throw_New_PListRadio(void)
{
	PListRadio* This = throw_mem_calloc(1, sizeof(*This));

	try
	{
		throw_PlayList_PlayList(&This->m_PlayList, &PListRadio_Cmds, Msg_Lookup("Play3"), NULL);

		throw_Task_AddListener(EEvent_OptionMixingParameters, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionMixingFrequency, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionMixingInterpol, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionMixingVolRamping, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionMixingBalance, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionMixingStereo, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionMixingEqualizer, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionParentalLock, PListRadio_Listener, This, false);
		throw_Task_AddListener(EEvent_OptionRescaleVolume, PListRadio_Listener, This, false);

		PListRadio_RefreshTrackList(This);
	}
	catch
	{
		Delete_PListRadio(This);
		throw_current();
	}
	catch_end

	return This;
}

PListRadio* PListRadio_Get(void)
{
	if (!App_pPListRadio) App_pPListRadio = throw_New_PListRadio();

	return App_pPListRadio;
}

void PListRadio_Release(void)
{
	if (App_pPListRadio)
	{
		Delete_PListRadio(App_pPListRadio);
		App_pPListRadio = NULL;
	}
}

/*-------------------------------------------------------------------------*
 *--- SSList calls --------------------------------------------------------*
 *-------------------------------------------------------------------------*/

const FLTrack* PListRadio_GetPlayedTrack(PListRadio* This)
{
	return This->m_pRefTrack;
}

void PListRadio_RemoveTrack(PListRadio* This, const FLTrack* pTrack)
{
	Player* pPlayer = This->m_PlayList.m_pPlayer;

	if (pPlayer)
		Player_RemoveTrack(pPlayer, pTrack);
}

void PListRadio_StopTrack(PListRadio* This, const FLTrack* pTrack)
{
	Player* pPlayer = This->m_PlayList.m_pPlayer;

	if (pPlayer
	&&  (This->m_pRefTrack == pTrack))
		CmdHandler_ExecCommand(&RadioPlayer_CmdHandler, This, Cmd_Ctrl_Stop);
}

void PListRadio_RefreshPlayer(PListRadio* This, unsigned int flags)
{
	Player* pPlayer = This->m_PlayList.m_pPlayer;

	if ((flags & player_refresh_track)
	&&  This->m_PlayList.m_pObject)
	{
		try
		{
			throw_MediaObject_MergeMeta(This->m_PlayList.m_pObject, FLTrack_GetMeta(This->m_pRefTrack));
		}
		catch
		{
		}
		catch_end
	}

	if (pPlayer && This->m_pEdit) Player_Refresh(pPlayer, flags);
}

void PListRadio_AttachFileList(PListRadio* This, FileList* pList)
{
	CmdHandler_ExecCommand(&RadioPlayer_CmdHandler, This, Cmd_Ctrl_Stop);

	This->m_pEdit = pList;

	PListRadio_RefreshTrackList(This);
}
