// @ts-check
// Fusion Research Driver - Music Module - Sync Management
//
// Copyright 2013-2021 Fusion Research, Inc.
// by Alexa Jade Fanti
//
// Comments:
//   This contains objects and data structures to support player synchronization.
//
//   Most critical is maintaining the sync group information and providing peer
//   information on demand. 
//
//   The sync groups are retrieved using the action "get_player_sync_groups"
//   Example: http://127.0.0.1:9720/oms/api/1/control/lms?action=get_player_sync_groups
//
//   This returns an XML that looks like this:
//     <FusionResearchLMS>
//       <syncgroups>
//         <group>
//           <player mac="98:fe:95:00:00:00"/>
//           <player mac="98:fe:95:00:00:01"/>
//           <player mac="98:fe:95:00:00:02"/>
//         </group>
//       </syncgroups>
//     </FusionResearchLMS>
//
//
// Depends: 
//   fusion_stringutils.js
//   fusion_misc.js
//   fusion_debugging.js
//   fusion_http.js
//   fusion_url.js
//   fusion_serverdiscovery.js
//
//System.Print("Fusion Driver - Music Module - Sync Management loaded...\r\n");


/******************************************************************
 * START Server Global Variables and Functions
 ******************************************************************/
/******************************************************************
 * END   Server Global Variables and Functions
 ******************************************************************/


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

SyncGroup.prototype.Initialize = function()
{
    this.members = new Array(); // list of player MACs in the group
}

SyncGroup.prototype.toString = function()
{
    var result = "";
    for (var x = 0; x < this.members.length; x++) {
        if (result != '') result += ', ';
        result += this.members[x];
    }
    return result;
}

SyncGroup.prototype.Add = function(playerMac)
{
    this.members.push(playerMac);
}
/******************************************************************
 * END   SyncGroup Class
 ******************************************************************/


/******************************************************************
 * START SyncTool Class
 ******************************************************************/
function SyncTool(owningServer)
{
    this._owner = owningServer;
    this.Initialize();
    this._timer_fetch = new Timer(); // Used to delay fetching for a little bit!
}

SyncTool.prototype.Initialize = function()
{
    this.ipaddress = OperationalServerIP();
    this.groups = new Array(); // read from the server... array of SyncGroup
}

// We want to support several types of "fetching". Sometimes we may need the sync group
// info right away, in which case we want NO delays whatsoever. This means we need to 
// make a blocking call to get the server's info and parse the XML. This SHOULD be rare 
// though. When we don't need that, a standard HTTP request (and callback) is fine, but 
// when we're doing that, there's a delay and we won't have the data after the initial
// call returns, instead we'll get called back later and have the info then.
// In this case, we are also conserned that because we'll get multiple sync events through 
// the server's feedback mechanism, we may be calling (and fetching) the SAME information 
// over and over, which is inefficient and slows everything down. To try to prevent that, 
// we'll institute a "fetch timer" that gets set on the fetch call (if we're NOT immediate)
// and we'll keep resetting it every time we're called. That way, we don't do anything till 
// we have a little break and believe our cusatomers have settled down.
SyncTool.prototype.Fetch = function(delay_in_msec)
{
    delay_in_msec = parseInt(delay_in_msec, 10); // Parse the delay to a number
    if (isNaN(delay_in_msec)) delay_in_msec = ReadConfig_AsInt("FetchDelay_SyncGroups_InMSec", 500, 0, 5000); // If delay was invalid, fall back to the default!
    if (delay_in_msec < 0) delay_in_msec = 0; // No delay of it's negative!

    if (delay_in_msec > 0) {
        // We're going to delay in case there are a bunch of fetch requests right after each other!

        if (this._timer_fetch.State == 1) {
            DebugSyncTool("Fetch : Timer is already running... stopping!", g_DEBUG_LEVEL_TRACE);
            DebugSyncTool("------------------------ Previous Fetch aborted!!! (that's a good thing)", g_DEBUG_LEVEL_TRACE);
            this._timer_fetch.Stop();
        }
        
        DebugSyncTool("Fetch : Starting timer for " + delay_in_msec + " milliseconds...", g_DEBUG_LEVEL_TRACE);
        this._timer_fetch.Start(SyncTool_OnTimer_InternalFetch, delay_in_msec, this);
    } else {
        DebugSyncTool("Fetch : Immediately (blocking)..!", g_DEBUG_LEVEL_TRACE);
        
        var url = GetURL_ServerRequestEx("get_player_sync_groups", "", "", "");
        var xml = HTTP_SYS_GetURL_AsXML_Blocking(url);
        this.InternalParseXML(xml);
    }
}

// This global function is called by our timer(s) and just calls the InternalFetch for the instance!
function SyncTool_OnTimer_InternalFetch(instance)
{
    DebugSyncTool("SyncTool_OnTimer_InternalFetch", g_DEBUG_LEVEL_TRACE);
    instance.InternalFetch();
}

SyncTool.prototype.InternalFetch = function()
{
    this.Initialize(); // We plan to replace this... let's clear out here

    DebugSyncTool("InternalFetch : Fetching sync groups...", g_DEBUG_LEVEL_TRACE);
    if (this.ipaddress == '') {
        DebugSyncTool("Fetch : Can't proceed without server IP address", g_DEBUG_LEVEL_CRITICAL);
        return;
    }

    var url = GetURL_ServerRequestEx("get_player_sync_groups", "", "", "");
    DebugSyncTool("InternalFetch : Fetching sync groups with URL = " + url, g_DEBUG_LEVEL_TRACE);

    var httpReq = new HttpRequest(url);
    httpReq.SetTag(this);
    httpReq.SetCallback(SyncTool_Callback_InternalParseXML);
    httpReq.Fetch();
}

// This is for HTTP Object Callbacks... we'll call out "InternalParseXML" method for our "Tag" (object)
function SyncTool_Callback_InternalParseXML(httpRequest, httpResponse)
{
    var xml = httpResponse.GetBody_AsXML();
    var me = httpRequest.GetTag();
    me.InternalParseXML(xml);
}

// This is eventually called by the HTTP engine when we have the response to a fetch request.
// We have to parse the XML and recode our information and trigger events as things happen.
SyncTool.prototype.InternalParseXML = function(xml)
{
    this.Initialize(); // We plan to replace this... let's clear out here

    if (xml === undefined) {
        DebugSyncTool("InternalParse XML is missing", g_DEBUG_LEVEL_ERROR);
        return;
    }

    //DebugSyncTool("Fetch : XML = \r\n" + xml + "\r\n --------------------", g_DEBUG_LEVEL_TRACE);

    this.groups = new Array(); // read from the server... array of SyncGroup
    try {
        var groups = xml.syncgroups.group;
        DebugSyncTool("InternalParseXML : I have " + groups.length() + " groups...", g_DEBUG_LEVEL_TRACE);

        for (var x = 0; x < groups.length(); x++) {
            var macs = groups[x].player;
            DebugSyncTool("InternalParseXML : Group " + x + " has " + macs.length() + " members...", g_DEBUG_LEVEL_TRACE);

            var sg = new SyncGroup();
            for (var y = 0; y < macs.length(); y++) sg.Add(macs[y].@mac.toLowerCase());
            this.groups.push(sg);
        }

        this.ReportSynchronization();

        DebugSyncTool("InternalParseXML : Retrieved " + this.Description(), g_DEBUG_LEVEL_INFO);
    } catch(err) {
        DebugSyncTool("InternalParseXML failed with error: " + err, g_DEBUG_LEVEL_ERROR);
    }
}

SyncTool.prototype.toString = function()
{
    return this.ipaddress;
}

SyncTool.prototype.Description = function(separator)
{
    if (separator === undefined) separator = ' | ';

    if (this.groups.length <= 0) return "No groups";
    if (this.groups.length == 1) {
        var result = "1 group: ";
        var sg = this.groups[0];
        for (var y = 0; y < sg.members.length; y++) {
            result = result + sg.members[y];
            if (y < (sg.members.length -1)) result = result + ', ';
        }
        return result;
    } else {
        var result = this.groups.length + " groups" + separator;
        for (var x = 0; x < this.groups.length; x++) {
            var sg = this.groups[x];
            result = result + x + ' = ';
            for (var y = 0; y < sg.members.length; y++) {
                result = result + sg.members[y];
                if (y < (sg.members.length -1)) result = result + ', ';
            }
            result = result + separator;
        }
        return result;
    }
}

/* Testing the Description() function
    System.Print("---------------------");
    System.Print(g_SYNCTOOL.Description());
    System.Print("---------------------");
    g_SYNCTOOL.FakeIt1();
    System.Print(g_SYNCTOOL.Description());
    System.Print("---------------------");
    g_SYNCTOOL.FakeIt2();
    System.Print(g_SYNCTOOL.Description());
    System.Print("---------------------");
*/

SyncTool.prototype.FakeIt1 = function()
{
    this.Initialize();
    var sg = new SyncGroup();
    sg.Add('98:fe:95:00:00:00');
    sg.Add('98:fe:95:00:00:01');
    sg.Add('98:fe:95:00:00:02');
    this.groups.push(sg);
}

SyncTool.prototype.FakeIt2 = function()
{
    this.Initialize();
    var sg = new SyncGroup();
    sg.Add('98:fe:95:00:00:00');
    sg.Add('98:fe:95:00:00:01');
    sg.Add('98:fe:95:00:00:02');
    this.groups.push(sg);
    var sg = new SyncGroup();
    sg.Add('98:fe:95:00:00:0A');
    sg.Add('98:fe:95:00:00:0B');
    this.groups.push(sg);
}


SyncTool.prototype.ReportSynchronization = function()
{
    DebugSyncTool("ReportSynchronization : Looking through all players to report sync status", g_DEBUG_LEVEL_TRACE);
    if ((this._owner === undefined) || (this._owner == null)) {
        DebugSyncTool("ReportSynchronization : No server defined so no report possible", g_DEBUG_LEVEL_WARN);
        return;
    }

    for (var x = 0; x < this._owner.queues.length; x++) {
        var queue = this._owner.queues[x];
        DebugSyncTool("ReportSynchronization : Looking for queue " + x + " : " + queue._playerMac + "...", g_DEBUG_LEVEL_TRACE);

        var groupIndex = this.FindGroup(queue._playerMac);

        if (groupIndex < 0) {
            DebugSyncTool("ReportSynchronization : Queue " + x + " : " + queue._playerMac + " has no peers", g_DEBUG_LEVEL_TRACE);
            SysVar_WriteBool("SysVar_Player" + queue._playerIndex + "_B_Synced", false);
            SysVar_WriteInt("SysVar_Player" + queue._playerIndex + "_I_SyncedTo", 0);
        } else {
            DebugSyncTool("ReportSynchronization : Queue " + x + " : " + queue._playerMac + " is synced...", g_DEBUG_LEVEL_TRACE);
            SysVar_WriteBool("SysVar_Player" + queue._playerIndex + "_B_Synced", true);
            // Find the group the player is in so we can find the "master" of the group
            var group = this.groups[groupIndex];
            var masterMac = group.members[0];
            var masterQueue = this._owner.GetQueueByMAC(masterMac);
            if ((masterMac == undefined) || (masterMac == null)) {
                DebugSyncTool("ReportSynchronization : We can't find the queue info for master " + masterMac, g_DEBUG_LEVEL_ERROR);
            } else {
                SysVar_WriteInt("SysVar_Player" + queue._playerIndex + "_I_SyncedTo", (masterQueue._playerIndex +1));
            }
        }

    }
};

// This returns the index of the group in which this player resides (or -1 if it's not synced)
SyncTool.prototype.FindGroup = function(playerMac)
{
    var result = -1; // Not found!

    playerMac = playerMac.toLowerCase();
    DebugSyncTool("FindGroup : Looking for the group containaing " + playerMac, g_DEBUG_LEVEL_TRACE);

    if (this.groups.length <= 0) {
        DebugSyncTool("FindGroup : There are no sync groups, so there can be group containing " + playerMac, g_DEBUG_LEVEL_INFO);
        return result;
    }

    var x = 0;
    while ((result == -1) && (x < this.groups.length)) {
        var sg = this.groups[x];
        var y = 0;
        while ((result == -1) && (y < sg.members.length)) {
            var memberMac = sg.members[y];
            DebugSyncTool("FindGroup : Looking in group " + x + " at member " + y + " which is " + memberMac, g_DEBUG_LEVEL_TRACE);

            if (playerMac == sg.members[y]) {
                result = x;
                DebugSyncTool("FindGroup : We found our match in group " + x + " at member " + y + " which is " + memberMac + " (for player " + playerMac + ")", g_DEBUG_LEVEL_INFO);
            }
            y++;
        }
        x++
    }

    if (result == -1) DebugSyncTool("FindGroup : We did NOT find ang group containing player " + playerMac, g_DEBUG_LEVEL_INFO);
    return result;
}

// This returns THE REST OF (not including the original) the players in a group!
SyncTool.prototype.FindPeers = function(playerMac)
{
    playerMac = playerMac.toLowerCase();
    var result = new Array();
    DebugSyncTool("FindPeers : Looking for the peers of " + playerMac + " using FindGroup", g_DEBUG_LEVEL_TRACE);

    var groupIndex = this.FindGroup(playerMac);

    // Then if we found a group, we add all BUT the player we're looking for!
    if (groupIndex > -1) {
        var sg = this.groups[groupIndex];

        DebugSyncTool("FindPeers : Assessing group " + groupIndex + " which has " + sg.members.length + " members", g_DEBUG_LEVEL_TRACE);
        for (var y = 0; y < sg.members.length; y++)
        {
            var memberMac = sg.members[y];
            if (playerMac != sg.members[y]) {
                DebugSyncTool("FindPeers : Assessing in group " + groupIndex + " member " + y + " (" + memberMac + ") is NOT the playerMac (" + playerMac + ")... so we'll add it!!!", g_DEBUG_LEVEL_TRACE);
                result.push(sg.members[y]);
            } else {
                DebugSyncTool("FindPeers : Assessing in group " + groupIndex + " member " + y + " (" + memberMac + ") is the player, so we ignore it.", g_DEBUG_LEVEL_TRACE);
            }
        }

        // Report what we did!
        if (result.length <= 0) {
            DebugSyncTool("FindPeers : Although we found our player '" + playerMac + "' in group " + groupIndex + ", somehow there were no peers", g_DEBUG_LEVEL_INFO);
        } else {
            var description = "";
            for (var x = 0; x < result.length; x++) {
                description = description + result[x];
                if (x < (result.length -1)) description = description + ', ';
            }
            DebugSyncTool("FindPeers : Player '" + playerMac + "' has the following peers: " + description, g_DEBUG_LEVEL_INFO);
        }
    } else {
        DebugSyncTool("FindPeers : Player '" + playerMac + "' is not a member of any sync group", g_DEBUG_LEVEL_INFO);
    }

    return result;
}

/******************************************************************
 * END   Server Class
 ******************************************************************/
