// @ts-check
// Fusion Research Driver - Music Module - Server
//
// Copyright 2021 Fusion Research, Inc.
// by Alexa Jade Fanti
//
// Comments:
//   This contains a single "server" object which we'll create globally to contain
//   player information and perform server related functions (i.e. reboot).
//
// Depends: 
//   fusion_stringutils.js
//   fusion_misc.js
//   fusion_debugging.js
//   fusion_sys_config.js
//   fusion_http.js
//   fusion_url.js
//   fusion_serverdiscovery.js
//
//System.Print("Fusion Driver - Music Module - Server loaded...\r\n");


/******************************************************************
 * START Server Global Variables and Functions
 ******************************************************************/
function SysFunc_SelectPlayer(index, view)
{
    // If this is an Inc or Dec we need to find the current index and change it!

	var count = ReadConfig_AsInt("Player_Count", 0);
    if ((index == '-1') || (index == '+1')) {
    	var oldindex = g_SERVER.view_player_map.GetPlayerIndexForViewID(view);
	    if (index == '-1') { // Dec
	    	index = oldindex -1;
	    	if (index < 0) index = count -1;
			Debug_SysFuncs("SelectPlayer decrement " + oldindex + " to " + index, g_DEBUG_LEVEL_TRACE);
	    } else { // Inc
	    	index = oldindex +1;
	    	if (index >= count) index = 0;
			Debug_SysFuncs("SelectPlayer increment " + oldindex + " to " + index, g_DEBUG_LEVEL_TRACE);
	    }
    }

    // First, find the new player 
	Debug_SysFuncs("SelectPlayer recording player " + index + " for view " + view, g_DEBUG_LEVEL_TRACE);
    g_SERVER.view_player_map.RecordSelectedPlayer(view, index);

	var player = g_SERVER.GetSpecifiedPlayerByIndex(index);
	if (player !== undefined) {
		player.ReportNames_ToView(view);
	}

	var queue = g_SERVER.GetQueueByIndex(index);
	if (queue !== undefined) {
		queue.ReportIndex_ToView(view);
		queue.ReportCount_ToView(view);
		queue.ReportTrack_ToView(view);
		queue.ReportArtist_ToView(view);
		queue.ReportAlbum_ToView(view);
		queue.ReportGenre_ToView(view);
		//queue.ReportTitle_ToView(view);
		//queue.ReportPath(view);
		queue.ReportPath_ToView(view);
		queue.ReportPosition_ToView(view);
		queue.ReportDuration_ToView(view);
		queue.ReportArt_ToView(view);
		queue.ReportCanRate_ToView(view);
		queue.ReportPower_ToView(view);
		queue.ReportTransport_ToView(view);
		queue.ReportVolume_ToView(view);
		queue.ReportMute_ToView(view);
		queue.ReportRepeat_ToView(view);
		queue.ReportShuffle_ToView(view);
		queue.ReportEntries_ToView(view);
	}
}
/******************************************************************
 * END   Server Global Variables and Functions
 ******************************************************************/


/******************************************************************
 * START Server Global Timer (for Queue Playback Timing)
 ******************************************************************/
// We want to see progress of the queue(s) if things are playing.
// To do that, we need a timer that will tickle the server and prompt it
// to check the queue transport state and fetch time information if necessary.
var g_SERVER_QUEUE_PLAYING_TIMER = new Timer();

function ServerTimer_Start()
{
	g_SERVER_QUEUE_PLAYING_TIMER.Start(OnServerQueuePlayingTimer, 1000);
}

function ServerTimer_Stop()
{
	g_SERVER_QUEUE_PLAYING_TIMER.Stop();
}

function OnServerQueuePlayingTimer()
{
	if (g_SERVER !== undefined) g_SERVER.OnQueuePlayingTimer();
	g_SERVER_QUEUE_PLAYING_TIMER.Start(OnServerQueuePlayingTimer, 1000);
}
/******************************************************************
 * END   Server Global Timer (for Queue Playback Timing)
 ******************************************************************/


/******************************************************************
 * START Player Class
 ******************************************************************/
function Player(owningServer, mac, server, ip, port, name, model, can_power_off, power, volume_fixed)
{
  	this.Initialize();
	this._owner = owningServer;

	if ((mac !== undefined)           && (mac != null))           this.mac         = mac;
	if ((server !== undefined)        && (server != null))        this.server      = server;
	if ((ip !== undefined)            && (ip != null))            this.ip          = ip;
	if ((port !== undefined)          && (port != null))          this.port        = parseInt(port, 10);
	if ((name !== undefined)          && (name != null))          this.name        = name;
	if ((model !== undefined)         && (model != null))         this.model       = model;
	if ((can_power_off !== undefined) && (can_power_off != null)) this.canPowerOff = parseBool(can_power_off);
	if ((power !== undefined)         && (power != null))         this.power       = parseBool(power);
	if ((volume_fixed !== undefined)  && (volume_fixed != null))  this.volumeFixed = parseBool(volume_fixed);
}

Player.prototype.Initialize = function()
{
	this.mac         = "";
	this.server      = "";
	this.ip          = "";
	this.port        = 0;
	this.name        = "";
	this.model       = "";
	this.canPowerOff = true;
	this.power       = true;
	this.volumeFixed = true;

	this.username = ""; // This is the name the integrator gives us from the config!
	this.index    = -1; // The index this player occupies in an array (0 based)
}

Player.prototype.getID = function()
{
	return "Player (" + this.index + ":" + this.mac + ")";
}

Player.prototype.toString = function()
{
	var result = this.name;
    if (this.username != "") result = result + " (aka '" + this.username + "')";
    if (this.index > -1) result = result + " (index = " + this.index + ")";

    result = result + ' [' + this.mac + '] @ ' + this.ip;
	return result;
//    return this.name + ' (' + this.mac + ') @ ' + this.ip;
}


Player.prototype.setName = function(newName)
{
	this.name = newName;
	this.ReportNames();	
}

//==============> START  Reporting Methods
// Reporting methods are responsible for updating the RTI System Variables and Events 
// as things change for the Queue object. They report what has already been set elsewhere.
// This means we need to "record" things BEFORE "reporting" them.
// These handle reporting for the specific player/queue (by index) as well 
// as reporting to ANY RTI System Views the player/queue may be selected for.

// Report Names...
Player.prototype.ReportNames_ToView = function(viewID)
{
	viewID = parseInt(viewID, 10);
	if (!isNaN(viewID)) {
		DebugServer(this.getID() + " ReportNames_ToView (view = " + viewID + ")", g_DEBUG_LEVEL_TRACE);

	    SysVar_Write("SysVar_PlayerS_S_RealName%" + viewID, this.name);
    	SysVar_Write("SysVar_PlayerS_S_UserName%" + viewID, this.username);

		var count = ReadConfig_AsInt("Player_Count", 0);
	    for (var x = 0; x < count; x++) {
    		if (x == this.index) SysVar_WriteBool("SysVar_PlayerS_B_Player" + x + "%" + viewID, true)
			else SysVar_WriteBool("SysVar_PlayerS_B_Player" + x + "%" + viewID, false);
    	}
	} else {
		DebugServer(this.getID() + " ReportNames_ToView parameter 'viewID' is invalid!", g_DEBUG_LEVEL_ERROR);
	}
};

Player.prototype.ReportNames = function()
{
	DebugServer(this.getID() + " ReportNames (main)", g_DEBUG_LEVEL_TRACE);
   	SysVar_Write("SysVar_Player" + this.index + "_S_RealName", this.name);
    SysVar_Write("SysVar_Player" + this.index + "_S_UserName", this.username);

	var viewIDs = new Array();
	if ((this._owner !== undefined) && (this._owner !== null) && (typeof(this._owner) == 'object')) {
		viewIDs = this._owner.GetArrayOfViewsForIndex(this.index);
	}
	for (var x = 0; x < viewIDs.length; x++) this.ReportNames_ToView(viewIDs[x]);
};
/******************************************************************
 * END   Player Class
 ******************************************************************/



/******************************************************************
 * START Server Class
 ******************************************************************/
function Server()
{
  	this.Initialize();
}

Server.prototype.Initialize = function()
{
    this.version_api = "";
	this.version_os  = "";
	this.oem_id      = "";
	this.oms_model   = "";
	this.model       = "";

	this.are_all_players_ready = false; // This was added 2023-05-12 to support connecting IF the controller is running before the music server

  	this.discovered_players = new Array(); // read from the server
  	this.specified_players = new Array(); // Entered in the configuration by the integrator
//  	this.queues = new Array(); // There should be one for each specified_player!!!

  	// Remember a view maps to a single player, but the same player can be referenced by multiple views!

  	// This is an array where the index is 0 - specified_players.length -1
  	// The value at that position is a view?
  	this.view_player_map = new ViewPlayerMap();
  	this.queues = new Array(); // There should be one for each specified_player!!!

  	this.sync_tool = new SyncTool(this); // We'll use this to maintain sync info!
	this.sync_tool.Fetch(); // We need to get initial status!

  	this.favorite_tool = new FavoriteTool(this); 
}

Server.prototype.CanDisplayListArt = function()
{
//	return ((this.oem_id == 'RTI') || (this.oms_model == 'MS3'));
	return (this.oem_id == 'RTI');
}



Server.prototype.Fetch_ArePlayersReady = function()
{
	DebugServer("Fetch_ArePlayersReady: Fetching player ready status...", g_DEBUG_LEVEL_TRACE);
	if (OperationalServerIP() == '') {
		DebugServer("Fetch_ArePlayersReady: Can't proceed without server IP address", g_DEBUG_LEVEL_CRITICAL);
		return;
	}
    var url = GetURL_ServerPrefix() + '/oms/api/1/manage/squeezelite?action=are_all_players_ready';

    var httpReq = new HttpRequest(url);
    httpReq.SetTag(this);
    httpReq.SetCallback(function(httpRequest, httpResponse) {
		var me = httpRequest.GetTag();
		me.Parse_ArePlayersReady(httpResponse.GetResponseCode());
    });
    httpReq.Fetch();
}

Server.prototype.Parse_ArePlayersReady = function(code)
{
	if ((code === undefined) || (code === null)) {
		DebugServer("Parse_ArePlayersReady: Response code is null or undefined", g_DEBUG_LEVEL_ERROR);
		return;
	}

	if (code == 200) {
		this.are_all_players_ready = true;
		DebugServer("Parse_ArePlayersReady: TRUE", g_DEBUG_LEVEL_INFO);
	} else {
		this.are_all_players_ready = false;
		DebugServer("Parse_ArePlayersReady: FALSE", g_DEBUG_LEVEL_INFO);
	}
}

Server.prototype.Fetch_Versions = function()
{
	DebugServer("Fetch_Versions: Fetching Server Version Information...", g_DEBUG_LEVEL_TRACE);
	if (OperationalServerIP() == '') {
		DebugServer("Fetch_Versions: Can't proceed without server IP address", g_DEBUG_LEVEL_CRITICAL);
		return;
	}
    var url = GetURL_ServerRequestEx("get_version", "", "", "");

    var httpReq = new HttpRequest(url);
    httpReq.SetTag(this);
    httpReq.SetCallback(function(httpRequest, httpResponse) {
		var me = httpRequest.GetTag();
        me.Parse_Versions(httpResponse.GetBody_AsXML());
    });
    httpReq.Fetch();
}

Server.prototype.Parse_Versions = function(xml)
{
	if ((xml === undefined) || (xml === null)) {
		DebugServer("Parse_Versions: XML is null or undefined", g_DEBUG_LEVEL_ERROR);
		return;
	}

	DebugServer("Parse_Versions XML:\r\n " + xml + "\r\n---------------------", g_DEBUG_LEVEL_TRACE);

	var power = (xml !== undefined);
    this.version_api = "";
	this.version_os  = "";
	this.oem_id      = "";
	this.oms_model   = "";
	this.model       = "";

    if (power) {
		this.version_api = xml.version.@api.toString();
		DebugServer("Parse_Versions: API version = " + this.version_api, g_DEBUG_LEVEL_INFO);

		this.version_os = xml.version.toString().trim();
		DebugServer("Parse_Versions: OS version = " + this.version_os, g_DEBUG_LEVEL_INFO);

		this.oem_id      = xml.version.@oem_id.toString();
		DebugServer("Parse_Versions: OEM ID = " + this.oem_id, g_DEBUG_LEVEL_INFO);

		this.oms_model   = xml.version.@oms_model.toString();
		DebugServer("Parse_Versions: OMS Model = " + this.oms_model, g_DEBUG_LEVEL_INFO);

		this.model       = xml.version.@model.toString();
		DebugServer("Parse_Versions: Model = " + this.model, g_DEBUG_LEVEL_INFO);
    } else DebugServer("Parse_Versions: Unable to retrieve version info... assuming server is off!", g_DEBUG_LEVEL_INFO);

	SysVar_WriteBool("SysVar_Server_B_Power", power);
	SysVar_Write("SysVar_Server_S_APIVersion", this.version_api); 
	SysVar_Write("SysVar_Server_S_OSVersion", this.version_os);
	SysVar_Write("SysVar_Server_S_Model", this.model);
}

Server.prototype.Fetch_Players = function()
{
	DebugServer("Fetch_ArePlayersReady: Fetching players...", g_DEBUG_LEVEL_TRACE);
	if (OperationalServerIP() == '') {
		DebugServer("Fetch_ArePlayersReady: Can't proceed without server IP address", g_DEBUG_LEVEL_CRITICAL);
		return;
	}
    var url = GetURL_ServerRequestEx("get_player_list", "", "", "");

    var httpReq = new HttpRequest(url);
    httpReq.SetTag(this);
    httpReq.SetCallback(function(httpRequest, httpResponse) {
		var me = httpRequest.GetTag();
		me.Parse_Players(httpResponse.GetBody_AsXML());
    });
    httpReq.Fetch();
}

Server.prototype.Parse_Players = function(xml)
{
	if ((xml === undefined) || (xml === null)) {
		DebugServer("Parse_Players: XML is null or undefined", g_DEBUG_LEVEL_ERROR);
		return;
	}

	DebugServer("Parse_Players XML:\r\n " + xml + "\r\n---------------------", g_DEBUG_LEVEL_TRACE);

  	this.discovered_players = new Array(); // initialize the list!
	try {
		var items = xml.players.player;
		for (var x = 0; x < items.length(); x++) {
			var player = this.InternalParse_PlayerItem(items[x]);
			player.index = x;
			DebugServer("Discovered player: " + player.toString(), g_DEBUG_LEVEL_INFO);
			this.discovered_players.push(player);
		}

		this.favorite_tool.Fetch(); // Now that we have at least one player, get the favorites too!
	} catch(err) {
		DebugServer("Parse_Players failed with error: " + err, g_DEBUG_LEVEL_ERROR);
	}
}


// This parses an XML entry from the server for a specific player and returns a player object
Server.prototype.InternalParse_PlayerItem = function(item)
{
	try {
		var player = new Player(this,
								item.@mac.toString().toLowerCase(),
								item.@server.toString(), 
								item.@ip.toString(), 
								item.@port.toString(), 
								System.ConvertFromUTF8(item.@name.toString()), 
								item.@model.toString(), 
								item.@can_power_off.toString(), 
								item.@power.toString(), 
								item.@volume_fixed.toString());
		return player;
	} catch(err) {
		DebugServer("InternalParse_PlayerItem failed with error: " + err, g_DEBUG_LEVEL_ERROR);
	}
}


// This populates the specified_players. If we have discovered players we use them, 
// otherwise we try to fetch player info one by one using the MAC.
Server.prototype.DefinePlayers = function()
{
  	this.specified_players = new Array(); // Entered in the configuration by the integrator

	var count = ReadConfig_AsInt("Player_Count", 0);
	if (count == 0) {
		DebugServer("DefinePlayers is not going to do anything because there are none defined!", g_DEBUG_LEVEL_WARN);
		return;
	}

	for (var x = 0; x < count; x++) {
		var name = "Player " + (x +1);// ReadConfig_AsString("Player" + x + "_Name", "");
		var mac  = ReadConfig_AsString("Player" + x + "_MAC", "");
		
		// We need to make sure we don't already have this player in the defined players (by MAC)
		// This is because we allocate some RTI resoures for a queue based on MAC and they're finite!
		var previous = this.GetSpecifiedPlayerByMAC(mac);
		if (previous !== undefined) {
			DebugServer("DefinePlayers cannot add player " + x + " since the MAC address is a duplicate of a previously defined player", g_DEBUG_LEVEL_WARN);
			mac = '';
		}

		if ((mac != "") && (name != "")) {
			var player = this.GetDiscoveredPlayerByMAC(mac);
			if (player === undefined) {

			    var url = GetURL_ServerRequestEx("get_player_info", mac, "", "");
    			var xml = HTTP_SYS_GetURL_AsXML_Blocking(url);
   				try {
					player = this.InternalParse_PlayerItem(xml.players.player[0]);
					player.index = x;
					DebugServer("DefinePlayers Defined player " + x + " as " + player.toString(), g_DEBUG_LEVEL_TRACE);
				} catch(err) {
					DebugServer("DefinePlayers Player " + x + " could not be parsed from XML. Error = " + err, g_DEBUG_LEVEL_ERROR);
				}
			} else {
				// We found the player in our discovered array and are going to re-cycle that one!
				DebugServer("DefinePlayers Player " + x + " was found in the previously discovered players", g_DEBUG_LEVEL_TRACE);
			}

			if (player !== undefined) {
				player.username = name; // Remember the user's name for the player!
				player.index = x;

				DebugServer("DefinePlayers Player " + x + " added as " + player.toString(), g_DEBUG_LEVEL_TRACE);
				this.specified_players.push(player);
			}

		} else {
			DebugServer("DefinePlayers Player " + x + "'s name and/or MAC address is missing!", g_DEBUG_LEVEL_ERROR);
		}

	}
}


Server.prototype.ConstructEverything = function()
{
	DebugServer("ConstructEverything: Checking if we think the server is available...", g_DEBUG_LEVEL_TRACE);

	// See if we need to make any requests of the server...
	if (!this.are_all_players_ready) {
		//DebugServer("ConstructEverything: Checking if all players are ready...", g_DEBUG_LEVEL_TRACE);
		this.Fetch_ArePlayersReady();
	} else {
		DebugServer("ConstructEverything: Server has all players ready.", g_DEBUG_LEVEL_TRACE);
	}

	if (this.version_api == "") {
		//DebugServer("ConstructEverything: Fetching server version...", g_DEBUG_LEVEL_TRACE);
		this.Fetch_Versions();
	} else {
		DebugServer("ConstructEverything: Server API version is " + this.version_api, g_DEBUG_LEVEL_TRACE);
	}

	if (this.discovered_players.length == 0) {
		//DebugServer("ConstructEverything: Fetching players...", g_DEBUG_LEVEL_TRACE);
		this.Fetch_Players();
	} else {
		DebugServer("ConstructEverything: Server has " + this.discovered_players.length + " players.", g_DEBUG_LEVEL_TRACE);
	}



	// Verify we have everything we need to proceed...!
	
	// if we have no API version... can we just drop out here?
	if (!this.are_all_players_ready) {
		DebugServer("ConstructEverything: Canceling because not all players are ready!", g_DEBUG_LEVEL_WARN);
		return;
	}

	if (this.version_api == "") {
		DebugServer("ConstructEverything: Canceling because there's no server version!", g_DEBUG_LEVEL_WARN);
		return;
	}

	if (this.discovered_players.length == 0) {
		DebugServer("ConstructEverything: Canceling because we have no players!", g_DEBUG_LEVEL_WARN);
		return;
	}

	DebugServer("ConstructEverything: Defining players...", g_DEBUG_LEVEL_TRACE);
	this.DefinePlayers();

	// Original code!
	DebugServer("ConstructEverything: About to create Queues...", g_DEBUG_LEVEL_TRACE);
	this.queues = new Array();
	for (var x = 0; x < this.specified_players.length; x++) {
		var player = this.specified_players[x];
		SysVar_Write("SysVar_Player" + x + "_S_UserName", player.username);
		SysVar_Write("SysVar_Player" + x + "_S_RealName", player.name);

		DebugServer("ConstructEverything: Creating queue for " + player.mac, g_DEBUG_LEVEL_TRACE);
		var queue = new Queue(this, player.mac, player.index);
		this.queues.push(queue);
		queue.Fetch(); // This should set variables!
	}
	
	this.favorite_tool.Fetch();
	DebugServer("ConstructEverything: Completed.", g_DEBUG_LEVEL_INFO);
}




Server.prototype.toString = function()
{
    return OperationalServerIP();
}




Server.prototype.GetDiscoveredPlayerByMAC = function(mac)
{
	for (var x = 0; x < this.discovered_players.length; x++) {
		if (this.discovered_players[x].mac.toLowerCase() == mac.toLowerCase()) return this.discovered_players[x];
	}
	return undefined; // NOT found!
}

Server.prototype.GetAnyPlayerMAC = function()
{
	if (this.discovered_players.length > 0) return this.discovered_players[0].mac;
	if (this.specified_players.length > 0) return this.specified_players[0].mac;
	return "";
}

Server.prototype.GetSpecifiedPlayerByIndex = function(index)
{
	if ((index >= 0) && (index < this.specified_players.length)) {
		return this.specified_players[index];
	}
	return undefined; // NOT found!
}

Server.prototype.GetSpecifiedPlayerByMAC = function(mac)
{
	for (var x = 0; x < this.specified_players.length; x++) {
		if (this.specified_players[x].mac.toLowerCase() == mac.toLowerCase()) return this.specified_players[x];
	}
	return undefined; // NOT found!
}

Server.prototype.GetQueueCount = function()
{
	return this.queues.length;
}

Server.prototype.GetQueueByMAC = function(mac)
{
	for (var x = 0; x < this.queues.length; x++) {
		if (this.queues[x]._playerMac.toLowerCase() == mac.toLowerCase()) return this.queues[x];
	}
	return undefined; // NOT found!
}

Server.prototype.GetQueueByIndex = function(index)
{
	index = parseInt(index, 10);
	if ((index >= 0) && (index < this.queues.length)) {
		return this.queues[index];
	}
	return undefined; // NOT found!
}

Server.prototype.GetQueueByView = function(viewID)
{
	var index = this.view_player_map.GetPlayerIndexForViewID(viewID);
	if ((index >= 0) && (index < this.queues.length)) return this.queues[index];
	return undefined; // NOT found!
}

Server.prototype.GetQueueByIndexOrView = function(playerIndex, viewID)
{
    // We need the player ID first... If this is -1 we need to use the view to find the queue! 
    playerIndex = parseInt(playerIndex, 10);
    viewID = parseInt(viewID, 10);

    var result = undefined;

    if (playerIndex < 0) {
        result = this.GetQueueByView(viewID);
        if ((result != undefined) && (result != null)) DebugServer("GetQueueByIndexOrView : will use queue " + result.toString() + " on RTI System View '" + viewID + "'", g_DEBUG_LEVEL_TRACE);
    } else {
        result = this.GetQueueByIndex(playerIndex);
		if ((result != undefined) && (result != null)) DebugServer("GetQueueByIndexOrView : will use queue " + result.toString() + " for playerIndex = '" + playerIndex + "'", g_DEBUG_LEVEL_TRACE);
    }

    if ((result === undefined) || (result == null)) {
        DebugServer("GetQueueByIndexOrView : Cound not find a queue for playerIndex = '" + playerIndex + "' or on RTI System View '" + viewID + "'", g_DEBUG_LEVEL_ERROR);
    }

    return result;
}




Server.prototype.GetArrayOfViewsForIndex = function(index)
{
	return this.view_player_map.GetViewIDArrayForPlayerIndex(index);
}


Server.prototype.GetArrayOfViewsForQueue = function(queue)
{
	if ((queue === undefined) || (queue === null) || (typeof(queue) != 'object')) {
		DebugServer("GetArrayOfViewsForQueue: Parameter 'queue' is missing or invalid", g_DEBUG_LEVEL_WARN);
		return new Array(); // let's return an empty array so our caller doesn't have to check!
	}

	return this.view_player_map.GetViewIDArrayForPlayerIndex(queue._playerIndex);
}

Server.prototype.GetQueueListForView = function(viewID)
{
	return this.view_player_map.GetQueueListForViewID(viewID)
}

// This is called by a global timer and needs to "service" 
// all the queues that are in the "play" transport state!
Server.prototype.OnQueuePlayingTimer = function(queue)
{
	// Go through every Queue we know about and check it's 
	// transport state. If it's "playing" we need to fetch 
	// the queue's time.
	for (var x = 0; x < this.queues.length; x++) {
		var queue = this.queues[x];
		if (queue.transport == "play") {

			if (queue.IsNow_Airplay()) {
				queue.recordPositionAndDuration(0, 0); 
			} else {
				var method = ReadConfig_AsString('QueueProgress_ReportMethod', 'none');
				switch(method) {
					case 'none' : /*Do NOTHING!*/ break;
					case 'real' : queue.FetchTime(); break;
					case 'fake' : queue.recordPositionPlusOne(); break;
					default : /*Do NOTHING!*/ break;
				}
			}
		}
	}
}


//==============> START  Queue Event Handlers


// This is called by the fusion_feedback.js function HandleFeedback
// We need to process it here and update any objects as well as 
// trigger and RTI system events and set RTI system variables.
//
Server.prototype.ProcessFeedback = function(feedbackEvent) // Object
{ 
	if (feedbackEvent === undefined) {
		return;
	}
	if (!feedbackEvent.isValid()) {
		return;
	}
	DebugServer("ProcessFeedback : " + feedbackEvent.toString(), g_DEBUG_LEVEL_INFO);

	if (feedbackEvent.from == 'server') {
		switch(feedbackEvent.key) {
			case "favorites" : 
				if (feedbackEvent.value == 'changed') this.favorite_tool.Fetch();
				gLIBRARY.MakeDirty_Favorites();
				//Library_MakeDirty_Favorites();
				break;

			case "playlists" :
				if (feedbackEvent.dryrun) {
					DebugServer("ProcessFeedback : Ignoring playlist change because it's a dry-run", g_DEBUG_LEVEL_TRACE);
					return;
				}
				switch(feedbackEvent.value) {
					case "rename" : // A particular playlist was renamed
						DebugServer("ProcessFeedback : Playlist " + feedbackEvent.target + " renamed to '" + feedbackEvent.title + "'", g_DEBUG_LEVEL_TRACE);
						gLIBRARY.MakeDirty_Playlists();
//						Library_MakeDirty_Playlists();
						break;
					case "delete" : // A particular playlist was deleted
						DebugServer("ProcessFeedback : Playlist " + feedbackEvent.target + " was deleted", g_DEBUG_LEVEL_TRACE);
						gLIBRARY.MakeDirty_Playlists();
//						Library_MakeDirty_Playlists();
						break;
					case "edit-delete" : // A particular playlist was edited (track removed)
						DebugServer("ProcessFeedback : Playlist " + feedbackEvent.target + " has had item " + feedbackEvent.index + " removed", g_DEBUG_LEVEL_TRACE);
						gLIBRARY.MakeDirty_Playlists(feedbackEvent.target);
//						Library_MakeDirty_Playlists(feedbackEvent.target);
						break;
				}
				break;
		}
		return;
	}

	var player = this.GetSpecifiedPlayerByMAC(feedbackEvent.from);
	if (player !== undefined) {
		DebugServer("ProcessFeedback : Player " + player.toString(), g_DEBUG_LEVEL_TRACE);
	}

	// Because we don't always get feedback for all players in a sync group when something on the 
	// master changes... we need an array of queues (build from a sync group) to perform things on.
	var queues = new Array(); // All queues to take action on!

	// This is the queue for which we received the feedback!
	var queue = this.GetQueueByMAC(feedbackEvent.from);
	if (queue !== undefined) {
		DebugServer("ProcessFeedback : Queue " + queue.toString(), g_DEBUG_LEVEL_TRACE);

		/// Moved inside queue !== undefined on 2021-08-17 by AJF	
		queues.push(queue); // Add it to our array of queues!

		// Let's find the other queues in the sync group!
		var otherMacs = this.sync_tool.FindPeers(queue._playerMac);
		for (var x = 0; x < otherMacs.length; x++) {
			var peer = this.GetQueueByMAC(otherMacs[x]);
			if (peer !== undefined) {
				DebugServer("ProcessFeedback : Adding sync peer " + peer.toString(), g_DEBUG_LEVEL_TRACE);
				queues.push(peer);
			}
		}
	}

	// We're doing these for ALL queues that are synced!
	switch(feedbackEvent.key) {
		case "power"     : for (var x = 0; x < queues.length; x++) {  queues[x].recordPower(feedbackEvent.value)		}   break;
		case "transport" : for (var x = 0; x < queues.length; x++) {  queues[x].recordTransport(feedbackEvent.value)	}	break;
		case "volume"    : if (queue !== undefined) {  queue.recordVolume(feedbackEvent.value)		}   break; // only for the MAIN queue!
 		case "mute"      : if (queue !== undefined) {  queue.recordMute(feedbackEvent.value)		}	break; // only for the MAIN queue!
		case "shuffle"   : for (var x = 0; x < queues.length; x++) {  queues[x].recordShuffle(feedbackEvent.value)		}	break;
		case "repeat"    : for (var x = 0; x < queues.length; x++) {  queues[x].recordRepeat(feedbackEvent.value)		}   break;

		case "sync"      : { 
				this.sync_tool.Fetch();

				if (feedbackEvent.value == '-') {
					DebugServer("ProcessFeedback : Synchronization - Player " + feedbackEvent.from + " was un-synced", g_DEBUG_LEVEL_TRACE);

// System.Print('-----------------------------------------------------------');
// System.Print(' - Player ' + queue._playerMac + ' was un-synced');
// System.Print('-----------------------------------------------------------');
//					if (queue !== undefined) queue.Fetch();
				} else {
					DebugServer("ProcessFeedback : Synchronization - Player " + feedbackEvent.value + " was synced to " + feedbackEvent.from, g_DEBUG_LEVEL_TRACE);
					// we need to decode the player mac in the value... find a new queue and fetch that!
					var queue2 = this.GetQueueByMAC(feedbackEvent.value);
					if (queue2 !== undefined) {
//						queue2.Fetch();
					} else {
						DebugServer("ProcessFeedback : Unable to find  queue for " + feedbackEvent.value + " (to sync)", g_DEBUG_LEVEL_WARN);
					}

// System.Print('-----------------------------------------------------------');
// System.Print(' * Player ' + feedbackEvent.value + ' was synced!!!');
// System.Print('-----------------------------------------------------------');
				}

			} 
			break;

		case "queue" :
			switch(feedbackEvent.value) {
				case "newsong" :
					for (var x = 0; x < queues.length; x++) {  queues[x].FetchStatus();				}
					for (var x = 0; x < queues.length; x++) {  queues[x].recordTransport("play");	}
					break;

				case "loaded" :
				case "open" :
					for (var x = 0; x < queues.length; x++) {  queues[x].Fetch();					}
					for (var x = 0; x < queues.length; x++) {  queues[x].recordTransport("play");	}
					break;

				case "currentsong" :
					for (var x = 0; x < queues.length; x++) {  queues[x].FetchStatus();	}
					break;

				case "addtracks" :
				case "move" :
				case "delete" :
					for (var x = 0; x < queues.length; x++) {  queues[x].FetchEntries();	}
					break;

				case "cleared" :
					for (var x = 0; x < queues.length; x++) {  queues[x].Fetch();					}
					for (var x = 0; x < queues.length; x++) {  queues[x].recordTransport("stop");	}
					break;
			}		
			break;

		case "playlists" :
			if (feedbackEvent.dryrun) {
				DebugServer("ProcessFeedback : Ignoring playlist change because it's a dry-run", g_DEBUG_LEVEL_TRACE);
				return;
			}
			switch(feedbackEvent.value) {
				case "rename" : // A particular playlist was renamed
					DebugServer("ProcessFeedback : Playlist " + feedbackEvent.target + " renamed to '" + feedbackEvent.title + "'", g_DEBUG_LEVEL_TRACE);
					gLIBRARY.MakeDirty_Playlists();
//					Library_MakeDirty_Playlists();
					break;
				case "delete" : // A particular playlist was deleted
					DebugServer("ProcessFeedback : Playlist " + feedbackEvent.target + " was deleted", g_DEBUG_LEVEL_TRACE);
					gLIBRARY.MakeDirty_Playlists();
//					Library_MakeDirty_Playlists();
					break;
				case "edit-delete" : // A particular playlist was edited (track removed)
					DebugServer("ProcessFeedback : Playlist " + feedbackEvent.target + " has had item " + feedbackEvent.index + " removed", g_DEBUG_LEVEL_TRACE);
					gLIBRARY.MakeDirty_Playlists(feedbackEvent.target);
//					Library_MakeDirty_Playlists(feedbackEvent.target);
					break;
			}
			break;

		case "playername" :
			var player = this.GetSpecifiedPlayerByMAC(feedbackEvent.from);
			if (player !== undefined) { 
				DebugServer("ProcessFeedback : Player name " + feedbackEvent.target + " has had name changed to " + feedbackEvent.value, g_DEBUG_LEVEL_TRACE);
				player.setName(feedbackEvent.value);
			}
			break;

		// case "client"    :
		// 	if ((value == "new") || 
		// 		(value == "connect") || 
		// 		(value == "disconnect")) G_PLAYERS.Fetch();
		// 	break;
	}

}

Server.prototype.RenamePlaylist = function(playlistId, newName)
{
	if ((playlistId === undefined) || (playlistId == null)) {
		DebugServer("RenamePlaylist : Cannot rename playlist without 'playlistId'", g_DEBUG_LEVEL_ERROR);
		return;
	}

	if ((newName === undefined) || (newName == null)) {
		DebugServer("RenamePlaylist : Cannot rename playlist without 'newName'", g_DEBUG_LEVEL_ERROR);
		return;
	}

	var url = GetURL_ServerRequestPlaylist("playlist_rename", playlistId, newName);
	DebugServer("RenamePlaylist : Requesting rename of playlist " + playlistId + " to '" + newName + "' using URL = " + url, g_DEBUG_LEVEL_INFO);
    
    var httpReq = new HttpRequest(url);
    httpReq.Fetch();
	return true;
}

Server.prototype.DeletePlaylist = function(playlistId)
{
	if ((playlistId === undefined) || (playlistId == null)) {
		DebugServer("DeletePlaylist : Cannot delete playlist without 'playlistId'", g_DEBUG_LEVEL_ERROR);
		return;
	}

	var url = GetURL_ServerRequestPlaylist("playlist_delete", playlistId, "");
	DebugServer("DeletePlaylist : Requesting deletion of playlist " + playlistId + " using URL = " + url, g_DEBUG_LEVEL_INFO);
    
    var httpReq = new HttpRequest(url);
    httpReq.Fetch();
	return true;
}

Server.prototype.RemovePlaylistItem = function(playlistId, itemIndex)
{
	if ((playlistId === undefined) || (playlistId == null)) {
		DebugServer("RemovePlaylistItem : Cannot remove playlist item without 'playlistId'", g_DEBUG_LEVEL_ERROR);
		return;
	}
	
	itemIndex = parseInt(itemIndex, 10);
	if (isNaN(itemIndex)) {
		DebugServer("RemovePlaylistItem : Cannot remove playlist item without 'itemIndex'", g_DEBUG_LEVEL_ERROR);
		return;
	}

	var url = GetURL_ServerRequestPlaylist("playlist_remove_item", playlistId, itemIndex);
	DebugServer("RemovePlaylistItem : Requesting removal of item " + itemIndex + " (0-based) from playlist " + playlistId + " using URL = " + url, g_DEBUG_LEVEL_INFO);
    
    var httpReq = new HttpRequest(url);
    httpReq.Fetch();
	return true;
}
/******************************************************************
 * END   Server Class
 ******************************************************************/
