// Capture and Restore (cR) module
// captureRestore.js
   console.log('CR version 0.0');
// 4:17 PM Sat July 16, 2022
// Written by: James D. Miller

/*
This acts to save (capture) a concise representation of the system state for later restoration. 
The engine-related state is limited to position and velocity (translational and rotational). No
attempt is made to capture the collision-related state internal to the Box2D engine.

Dependencies:
   gwModule.js (gW.)
   constructorsAndPrototypes.js (cP.)
   utilities.js
*/

var cR = (function() {
   "use strict";
   
   // Names starting with m_ indicate module-scope globals.
   
   
   // module globals for objects brought in by initializeModule
   var x_canvas, x_ctx;
   
   ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
   ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
   
   function initializeModule( canvas, ctx) {
      x_canvas = canvas;
      x_ctx = ctx;
   }
   
   function json_scrubber( key, value) {
      /*
      Use this function to exclude the b2d objects in the stringify process. 
      Apparently the b2d and rtc objects have circular references that 
      stringify doesn't like. So have to regenerate the b2d objects in the 
      demo area when the json capture is restored. 

      Also have to avoid the client related addons: jet, gun, and shield. 
      These have references back their pucks, this too causes circular issues 
      for stringify. 

      Also remove keys like spo1 and spo2 (in Springs object) mainly to keep 
      the wordiness down; many keys are not needed in the reconstruction 
      process. 

      So be careful here: any key with a name in the OR list of json_scrubber 
      (see if block below) will be excluded from the capture. 
      */
      if ( (key == 'b2d') || (key == 'b2dSensor') || (key == 'rtc') || 
           (key == 'jet') || (key == 'gun') || (key == 'shield') || 
           (key == 'spo1') || (key == 'spo2') || 
           (key == 'jto1') || (key == 'jto2') || 
           (key == 'parsAtBirth') || 
           (key == 'puck') || (key.includes('key_')) || (key.includes('_scaling')) || (key.includes('selectionPoint')) || 
           (key == 'position_2d_px') || (key == 'nonCOM_2d_N') ) {
         return undefined;
      } else {
         return value;
      }
   }
   
   function saveState( pars = {} ) {
      var captureName = setDefault( pars.captureName, null);
      var dataForCleaning = setDefault( pars.dataForCleaning, null);
      var inhibitWriteToCaptureCell = setDefault( pars.inhibitWriteToCaptureCell, false);
      
      var timeString = new Date();
      
      if (dataForCleaning) {
         // Use an old capture, that is passed in via dataForCleaning in pars, as the data sources.
         // (see comment in cleanCapture)
         
         if ( [7,8].includes( dataForCleaning.demoIndex) ) {
            if ( ! dataForCleaning.startingPosAndVels) {
               dataForCleaning.startingPosAndVels = defaultStartingPosAndVel( dataForCleaning.demoVersion);
            }
         } else {
            // remove the key if not in the 7 or 8 demo groups
            delete dataForCleaning.startingPosAndVels;
         }
         
         // Add some canvas dimensions if needed.
         if ( ! (dataForCleaning.canvasDimensions)) {
            if (dataForCleaning.demoIndex == 8) {
               dataForCleaning.canvasDimensions = {'width':1250, 'height':950};
            } else {
               dataForCleaning.canvasDimensions = {'width':600, 'height':600};
            }
         }
       
         var tableState = {'demoIndex':dataForCleaning.demoIndex,
                           'demoVersion':dataForCleaning.demoVersion,
                           'date':timeString.toLocaleString(),
                           'canvasDimensions': {'width':dataForCleaning.canvasDimensions.width, 'height':dataForCleaning.canvasDimensions.height},
                           'gravity':dataForCleaning.gravity,
                           'comSelection':dataForCleaning.comSelection,
                           'fullScreenDemo':dataForCleaning.fullScreenDemo,
                           'lockedAndLoaded':dataForCleaning.lockedAndLoaded,
                           'globalCompositeOperation':dataForCleaning.globalCompositeOperation,
                           'wallMapData':dataForCleaning.wallMapData, 
                           'puckMapData':dataForCleaning.puckMapData, 
                           'pinMapData':dataForCleaning.pinMapData, 
                           'springMapData':dataForCleaning.springMapData,
                           'jointMapData':dataForCleaning.jointMapData,
                           'startingPosAndVels':dataForCleaning.startingPosAndVels,
                           'clients':dataForCleaning.clients};
                           
         if (dataForCleaning.piCalcs) {
            tableState = Object.assign({}, tableState, {'piCalcs':dataForCleaning.piCalcs} );
            if (dataForCleaning.piEngine) {
               tableState = Object.assign({}, tableState, {'piEngine':dataForCleaning.piEngine} );
            }
         } else {
            tableState = Object.assign({}, tableState, {'piCalcs':{}} );
         }
      
      // Get a fresh capture (i.e., using the live stuff as the data source).            
      } else {
         if ( ! inhibitWriteToCaptureCell) {
            if (captureName) {
               gW.setDemoVersion( gW.getDemoVersion() + '.' + captureName);
            } else {
               gW.setDemoVersion( gW.getDemoVersion() + '.' + Math.floor((Math.random() * 1000) + 1));
            }
            if (gW.getDemoIndex() != 0) {
               // Adjust the highlighting in the plus row (there's no plus row for demo 0)
               document.getElementById( gW.getDemoVersion().slice(0,1) + ".a").style = '';
               document.getElementById( gW.getDemoVersion().slice(0,3)).style = 'color:white; background-color:gray; border-style:solid; border-color:black; border-width:4px 0px 4px 0px';
            }
         }
         
         var tableState = {'demoIndex':gW.getDemoIndex(),
                           'demoVersion':gW.getDemoVersion(),
                           'date':timeString.toLocaleString(),
                           'capturedAtFrame':gW.getFrameCount(),
                           'canvasDimensions': {'width':x_canvas.width, 'height':x_canvas.height},
                           'gravity':gW.getG_ON(),
                           'comSelection':gW.dC.comSelection.checked,
                           'fullScreenDemo':gW.getFullScreenDemo(),
                           'lockedAndLoaded':gW.getLockedAndLoaded(),
                           'globalCompositeOperation':x_ctx.globalCompositeOperation,
                           'wallMapData':gW.aT.wallMap, 
                           'puckMapData':gW.aT.puckMap, 
                           'pinMapData':gW.aT.pinMap, 
                           'springMapData':gW.aT.springMap,
                           'jointMapData':gW.aT.jointMap,
                           'startingPosAndVels':cP.Client.startingPandV,
                           'clients':gW.clients};
                           
         // For demos using the piCalcEngine, add the engine state to the capture.
         if ( ['1.c','1.d','1.e'].includes( gW.demoVersionBase( gW.getDemoVersion())) ) {
            var piCalcs = {};
            piCalcs['clacks'] = gW.getPiCalcs().clacks;
            piCalcs['enabled'] = gW.getPiCalcs().enabled;
            piCalcs['usePiEngine'] = gW.getPiCalcs().usePiEngine;
            
            let piCalcEngine = gW.getPiCalcEngine();
            
            if (piCalcs['usePiEngine']) {
               cP.PiEngine.state['lastCollidedWithWall']       = piCalcEngine['lastCollidedWithWall'];
               cP.PiEngine.state['atLeastOneCollisionInFrame'] = piCalcEngine['atLeastOneCollisionInFrame'];
               cP.PiEngine.state['nFinerTimeStepFactor'] = piCalcEngine['nFinerTimeStepFactor'];               
               piCalcs['p1_v_max']       = piCalcEngine['p1_v_max'];
               piCalcs['collisionCount'] = piCalcEngine['collisionCount'];
               
            } else {
               piCalcs['p1_v_max']       = gW.aT.puckMap['puck1'].vmax;
               piCalcs['collisionCount'] = gW.aT.collisionCount;
            }
            
            tableState = Object.assign( {}, tableState, {'piCalcs':piCalcs}, {'piEngine':cP.PiEngine.state} );
         }
      
         // Here you still have full state info. That's because this is before the json_scrubber is called. So if you want to keep
         // something that's about to be scrubbed out, do it here. As an example: the angle of the NPC casting rays,
         // that are in the gun attributes. This can be put into the rayCast_init_deg attribute of the NPC's puck. Then it
         // will be used when the NPC's puck and gun are restored (the gun gets this from its puck).
         for (var p_key in tableState.puckMapData) {
            var puck = tableState.puckMapData[ p_key];
            if ((puck.clientName) && (puck.clientName.slice(0,3) == 'NPC')) {
               puck.rayCast_init_deg = puck.gun.rayCastLine_2d_m.get_angle();
            }
         }
      }
      
      // See comments in the json_scrubber function above.
      var table_JSON = JSON.stringify( tableState, json_scrubber, 3);
      
      // Parsing after JSON.stringify makes a deep copy, with no references back to the original objects. So can delete stuff without
      // mangling the current running demo.
      var tableState_copy = JSON.parse( table_JSON);
      
      // Remove some non-editable puck keys from all pucks.
      var generalPuckKeys = ['tail','age_ms','ageLimit_ms','radius_px','mass_kg','half_height_px','half_width_px','tempInhibitExtForce','spotted',
                             'lowBallFinderCircle_timerLimit_s','lowBallFinderCircle_timer_s','firstClientDirectMove','nameTip_timerLimit_s','nameTip_timer_s',
                             'sprDamp_force_2d_N','springOnly_force_2d_N','jet_force_2d_N','impulse_2d_Ns','navSpringOnly_force_2d_N','drawDuringPE',
                             
                             'poorHealthFraction','whoShotBullet','flash','inComing','flashCount','atLeastOneHit','stayOn',
                             'hitCount','deleted','clientNameOfShooter',
                             'bulletAgeLimit_ms' // this is a client attribute now
                             ];
                             
      // Remove these from pucks with no client controls.
      var simplePuckKeys =  ['disableJet','noRecoil','bullet_restitution','hitLimit'];
      
      // Remove these from clients depending on whether NPC or Human
      var NPC_clientPuckKeys = ['angleLine'];
      var nonNPC_clientPuckKeys = ['disableJet','rayCast_init_deg','rayRotationRate_dps','rayCastLineLength_m'];
      
      for (var p_key in tableState_copy.puckMapData) {
         var puck = tableState_copy.puckMapData[ p_key];
         
         // Turning off the machSwitch causes the puck to be restored to the captured velocity rather than a specified Mach value.
         if ((puck.tailPars) && ( ! dataForCleaning)) puck.tailPars.machSwitch = false;
         
         // Delete bullet and network-client pucks in Puck Popper captures
         if ((tableState_copy.demoIndex == 7) || (tableState_copy.demoIndex == 8)) {
            if ( (puck.bullet) || ((puck.clientName) && (puck.clientName.slice(0,1) == 'u')) ) {
               delete tableState_copy.puckMapData[ p_key];
               continue; // go directly to next key in this for loop
            } 
         }
         
         // Delete keys on pucks:
         
         // All pucks
         for (var key of generalPuckKeys) {
            delete puck[ key];
         }
         // Simple pucks (no client controls)
         if ( ! puck.clientName) {
            for (var key of simplePuckKeys) {
               delete puck[ key];
            }
         // All human client pucks
         } else if (puck.clientName.slice(0,3) != 'NPC') {
            for (var key of nonNPC_clientPuckKeys) {
               delete puck[ key];
            }
         // All NPC client pucks
         } else if (puck.clientName.slice(0,3) == 'NPC') {
            for (var key of NPC_clientPuckKeys) {
               delete puck[ key];
            }
         }
      }
      
      // Remove some non-editable pin keys.  
      var pinKeys = ['radius_m'];
      for (var pin_key in tableState_copy.pinMapData) {
         var pin = tableState_copy.pinMapData[ pin_key];
         for (var key of pinKeys) {
            delete pin[ key];
         }
      }
      
      // Remove some non-editable wall keys.
      var wallKeys = ['deleted','half_height_px','half_width_px','firstClientDirectMove'];  // color
      for (var wall_key in tableState_copy.wallMapData) {
         var wall = tableState_copy.wallMapData[ wall_key];
         for (var key of wallKeys) {
            delete wall[ key];
         }
      }
      
      // Remove some spring keys.
      var springKeys = ['pinned','p1p2_separation_2d_m','p1p2_separation_m','p1p2_normalized_2d',
                        'spo1_ap_w_2d_m','spo1_ap_w_2d_px','spo2_ap_w_2d_m','spo2_ap_w_2d_px',
                        'selected','softConstraints_setInPars'];
      for (var spring_key in tableState_copy.springMapData) {
         var spring = tableState_copy.springMapData[ spring_key];
         
         // Don't capture the local ap (attach point) for pins.
         if (spring.p1_name.slice(0,3) == "pin") delete spring['spo1_ap_l_2d_m'];
         if (spring.p2_name.slice(0,3) == "pin") delete spring['spo2_ap_l_2d_m'];
         
         for (var key of springKeys) {
            delete spring[ key];
         }
      }
      // Remove some joint keys.
      var jointKeys = ['jto1_ap_w_2d_m','jto1_ap_w_2d_px','jto2_ap_w_2d_m','jto2_ap_w_2d_px','selected','colorInTransition'];  // color
      for (var joint_key in tableState_copy.jointMapData) {
         var joint = tableState_copy.jointMapData[ joint_key];
         for (var key of jointKeys) { 
            delete joint[ key];
         }
      }
      
      // For client objects, clean off all keys EXCEPT these (i.e. SAVE these): 
      var saveTheseDroneKeys = ['color','name','NPC_pin_timer_s','NPC_pin_timer_limit_s','bulletAgeLimit_ms'];
      var saveTheseHostKeys =  ['color','name','bulletAgeLimit_ms'];
      for (var client_key in tableState_copy.clients) {
         var client = tableState_copy.clients[ client_key];
         
         if ((client.name.slice(0,1) == 'u') || (client.name == 'manWithNoName')) {
            // Delete network clients...
            delete tableState_copy.clients[ client_key];
            
         } else if (client.name.slice(0,3) == 'NPC') {
            // Clean-up keys on drone clients.
            for (var clientKey in client) {
               if ( ! saveTheseDroneKeys.includes( clientKey)) {
                  delete client[ clientKey];
               }
            }
            
            // Add these keys if missing in an old capture...
            client.bulletAgeLimit_ms = (client.bulletAgeLimit_ms) ? client.bulletAgeLimit_ms : null;
            
         } else if (client.name == 'local') {
            // Clean-up keys on the host.
            for (var clientKey in client) {
               if ( ! saveTheseHostKeys.includes( clientKey)) {
                  delete client[ clientKey];
               }
            }
		 
            // Add these keys if missing in an old capture...
            client.color = (client.color) ? client.color : null;
            client.bulletAgeLimit_ms = (client.bulletAgeLimit_ms) ? client.bulletAgeLimit_ms : null;
         }
      }
      
      // Exit if state data was passed in to be cleaned.
      if (dataForCleaning) return tableState_copy;
      //----------------------------------------------------------------------
      
      // Once again, put it in a string...
      table_JSON = JSON.stringify( tableState_copy, null, 3);
      
      if ( ! inhibitWriteToCaptureCell) {
         // Write the json string to this visible input field.
         gW.dC.json.value = table_JSON;
         // Wait 0.5 seconds, then scroll the input field to the top.
         window.setTimeout( function() { scrollCaptureArea();}, 500);
         
         // Select, copy to clipboard, and then remove focus from the input field.
         gW.dC.json.select();
         document.execCommand('copy');
         window.getSelection().removeAllRanges(); // this is necessary for the blur method to work in MS Edge.
         gW.dC.json.blur();
      }

      return table_JSON;
   }
   
   function runCapture( pars={}) {
      let fromKeyBoard = setDefault( pars.fromKeyBoard, false);
      let state_capture, demoIndex;
      if (gW.dC.json.value != '') {
         try {
            state_capture = JSON.parse( gW.dC.json.value);
         } catch (err) {
            state_capture = null;
            window.alert("There's a formatting error in the state capture. Try clicking the 'Clear' button.");
         }
      } else {
         gW.messages['help'].newMessage('The capture text area is empty.', 2.0);
         state_capture = null;
      }
      
      let shift_key = gW.clients['local'].key_shift; // before the key-state reset that is in demoStart
      
      if (state_capture) { 
         demoIndex = state_capture.demoIndex;
         gW.demoStart( demoIndex, {'scrollCA':false});
         
         // grab a capture before the engine changes state...
         if ((shift_key == "D") && ( ! fromKeyBoard)) {
            saveState();
            gW.messages['help'].newMessage('The capture has been updated.', 2.0);
         }
         
      } else {
         // grab a capture before the engine changes state...
         if ((shift_key == "D") && ( ! fromKeyBoard)) {
            gW.demoStart( gW.getDemoIndex());
            saveState();
            gW.messages['help'].newMessage('A capture has been taken.', 2.0);
         }
         
         // Make sure the loop isn't pause so the help messages are displayed.
         if (gW.dC.pause.checked) {
            gW.dC.pause.checked = false;
            gW.setPauseState();
         }
      }
   }
   
   function clearState() {
      // Reset the capture state...
      gW.dC.json.value = '';
      // Reset the highlight styles in the row below the number buttons.
      var highlightedLinkInPlusRow = document.getElementById( gW.getDemoVersion().slice(0,3));
      var firstLinkInPlusRow = document.getElementById( gW.getDemoVersion().slice(0,1) + ".a");
      if (highlightedLinkInPlusRow) highlightedLinkInPlusRow.style = '';
      if (firstLinkInPlusRow) firstLinkInPlusRow.style = 'color:white; background-color:gray; padding:2px 0px';
   }
   
   function cleanCapture() {
      /*
      Clean up an old capture that's in the text area (or one of the "_fromFile" files).
      Mainly this removes old keys. You can enlist cleanCapture and saveState to add new editable keys (if it's in
      the code), but it is generally best to run the old capture and then immediately take a new capture.
      
      Recently I modified runCapture to be able to do that (immediately take a capture before the engine modifies state) if the shift key is down.
      
      cleanCapture can be run by middle clicking in the text area.
      */
      if (gW.getDemoVersion() == '8.a') {
         var state_data = demo_8_fromFile;
         
      } else if (gW.getDemoVersion() == '6.a') {
         var state_data = demo_6_fromFile;
         
      } else {
         if (gW.dC.json.value != "") {
            var state_data = JSON.parse( gW.dC.json.value);
         } else {
            gW.messages['help'].newMessage('No capture to update.', 2.0);
            return;
         }
      }
      
      // first, process (clean) the capture with saveState
      state_data = saveState( {'dataForCleaning':state_data} );
      
      // Special loop for pucks.
      for (var p_key in state_data.puckMapData) {
         var puck = state_data.puckMapData[ p_key];
         
         if (puck.clientName) {
            puck.groupIndex = -puck.name.slice(4) - 1000;
         } else {
            if ((state_data.demoVersion == '3.b') || (state_data.demoVersion == '3.c')) {
               // leave these alone, puck-puck collisions have been inhibited on these pucks.
            } else {
               puck.groupIndex = 0;
            }
         }
      }
      
      // For all the maps.
      var mapList = ['puckMapData','pinMapData','springMapData','jointMapData','wallMapData','clients'];
      for (var map of mapList) {
         for (var key in state_data[ map]) {
            var element = state_data[ map][ key];
            
            delete element['parsAtBirth'];
            
            delete element['alsoThese'];
            
            // Put the alsoThese key at the beginning of the object. Commented this
            // out for now. Could be useful if want to force an attribute to be recognized
            // in the capture.
            //state_data[ map][ key] = Object.assign({'alsoThese':[]}, element);
         }
      }
      
      gW.dC.json.value = JSON.stringify( state_data, null, 3);
      
      // Select, copy to clipboard, and then remove focus from the input field.
      gW.dC.json.select();
      document.execCommand('copy');
      window.getSelection().removeAllRanges(); // this is necessary for the blur method to work in MS Edge.
      gW.dC.json.blur();   
   
      gW.messages['help'].newMessage('The capture has been updated (scripted, without instantiation).', 2.0);
   }
   
   function newBirth( captureObj, type) {
      // Update the birth object (based on the capture state) and use it for restoration.
      var newBirthState = {}, par_list;
      
      // If there's a parameter that is getting into the capture but should be blocked in the birth process:
      var forgetList = {
         'puck': ['position_2d_m','velocity_2d_mps'], // These are explicitly passed to constructor via arguments (so not needed in birth object)
         'wall': ['position_2d_m'],  // Position is passed via arguments. Velocity can be specified in birth object.
         'pin':  ['position_2d_m'],  // Position is passed via arguments. Velocity can be specified in birth object.
         's':    [], // spring
         'j':    [], // joint
         'NPC':  []  // drones
      };
      for (var birthParm in captureObj) {
         if (!forgetList[ type].includes( birthParm)) {
            // If this parameter's value is a vector, instantiate it using Vec2D.
            // (Note the check for "null." Null is also an object in javascript, but does not have the hasownProperty method.)
            if ( ((typeof(captureObj[ birthParm]) === 'object') && (captureObj[ birthParm] !== null)) && 
                 ((captureObj[ birthParm].hasOwnProperty('x')) && (captureObj[ birthParm].hasOwnProperty('y'))) ) {
                   
                  newBirthState[ birthParm] = new cP.Vec2D( captureObj[ birthParm].x, captureObj[ birthParm].y);
               
            } else {
               newBirthState[ birthParm] = captureObj[ birthParm];
            }
         }
      }
      
      // For all types, override the default naming process, specify a name in the birth parameters. This gives
      // the new object the name used in the capture object. This is needed in reconstructing 
      // springs (that use the original puck name). This is also needed if pucks are
      // deleted in a jello matrix.
      if (captureObj.name) {
         newBirthState.name = captureObj.name;
      }
      return newBirthState;
   }   
   
   function restoreFromState( state_data) {
      try {
         // return the template that is returned from restoreFromState_main
         return restoreFromState_main( state_data);
         
      } catch (err) {
         gW.stopit();
         window.alert(gW.getDemoVersion() +
                     "\nUnable to restore this capture. " +
                     "\n   Possibly you've been boldly editing the JSON text." +
                     "\n   If so, please refine your edits or start from a new capture." +
                     "\n" +
                     "\n" + err.name +
                     "\nmessage:  " + err.message);
         gW.demoStart(0);
      }
   }
      
   function restoreFromState_main( state_data) {
      // Environmental parameters...
      
      // Must do canvas dimensions before setting x_ctx.globalCompositeOperation.
      if (typeof state_data.canvasDimensions !== "undefined") {
         //canvasDiv.style.width = state_data.canvasDimensions.width + "px";
         x_canvas.width =          state_data.canvasDimensions.width;
         
         //canvasDiv.style.height = state_data.canvasDimensions.height + "px";
         x_canvas.height =         state_data.canvasDimensions.height; 
      }
      
      if (state_data.globalCompositeOperation) {
         x_ctx.globalCompositeOperation = state_data.globalCompositeOperation;
      } else {
         x_ctx.globalCompositeOperation = 'source-over';
      }
      
      gW.clearCanvas();
      
      if (typeof state_data.demoVersion !== "undefined") {
         gW.setDemoVersion( state_data.demoVersion);
      }
      
      // Message the user if the COM setting is changed by the capture restore.
      if (typeof state_data.comSelection !== "undefined") {
         if ((gW.dC.comSelection.checked) && ( ! state_data.comSelection)) {
            gW.messages['help'].newMessage('Center of mass (COM) selection: [base,yellow]OFF[base]', 3.0);
            
         } else if (( ! gW.dC.comSelection.checked) && (state_data.comSelection)) {
            gW.messages['help'].newMessage('Center of mass (COM) selection: [base,yellow]ON[base]', 3.0);
         }
         gW.dC.comSelection.checked = state_data.comSelection;
         
      } else {
         if ( ! gW.dC.comSelection.checked) {
            gW.messages['help'].newMessage('Center of mass (COM) selection: [base,yellow]ON[base]', 3.0);
            gW.dC.comSelection.checked = true;
            //comSelection_Toggle(null, 2);
         }
      }
      
    
      // Rebuild the walls from the capture data.
      for (var wallName in state_data.wallMapData) {
         // wall references one specific wall (from the captured state)
         var wall = state_data.wallMapData[ wallName];
         // Create the new Wall and add it to the wallMap (via its constructor).
         new cP.Wall( wall.position_2d_m, newBirth( wall, 'wall'));
      }
      // Establish the name of the top leg of the fence (for use by the PiEngine).
      if ((cP.Wall.topFenceLegName == null) && (gW.aT.wallMap['wall1'])) {
         if (gW.aT.wallMap['wall1'].fence) {
            cP.Wall.topFenceLegName = 'wall1';
            //console.log("topFenceLegName=" + cP.Wall.topFenceLegName);
         } else {
            //console.log("wall1 is not part of the fence.");
         }
      } else {
         //console.log("topFenceLegName set by restore");
      }      
      
      // NPC and host clients...
      for (var clientName in state_data.clients) {
         var client = state_data.clients[ clientName];
         
         if (clientName.slice(0,3) == 'NPC') {
            new cP.Client( newBirth( client, 'NPC'));
            
         } else if (clientName == 'local') {
            // don't regenerate the host, it should still be there.
            gW.clients['local'].color = (client.color) ? client.color : null;
            //gW.clients['local'].nickName = (client.nickName) ? client.nickName : null;
            gW.clients['local'].bulletAgeLimit_ms = (client.bulletAgeLimit_ms) ? client.bulletAgeLimit_ms : null;
         }
      }
      
      // Rebuild the pins.
      for (var pinName in state_data.pinMapData) {
         // "pin" is one pin (captured state)
         var pin = state_data.pinMapData[ pinName];
         // Create the new Pin and add it to the pinMap (via its constructor).
         new cP.Pin( pin.position_2d_m, newBirth( pin, 'pin'));
      }
      
      // Rebuild the pucks (and the puck map).
      var localHostPuckName = null, networkClientName = null, puckNameForTemplate = null;
      
      for (var p_key in state_data.puckMapData) {
         // puck is a single puck (captured state)
         var puck = state_data.puckMapData[ p_key];
         
         // If there's a puck for the local host, record the name for use in returning a puck template.
         // Also snag a puck name from the network clients as a second option.
         if (puck.clientName == 'local') {
            localHostPuckName = puck.name;
         } else if ((puck.clientName) && (puck.clientName.slice(0,1) == 'u')) {
            networkClientName = puck.name;
         }
         
         // Now create the puck and give it the old name (see the end of the newBirth function).
         // The "Host player" option must be checked to enable the creation of a puck for the local client.
         // Network-client pucks are not recreation here (because it depends on active network clients for assignment).
         if ( (!(puck.bullet && (gW.getDemoIndex() == 7 || gW.getDemoIndex() == 8))) &&   // NOT a game bullet AND 
              ( (puck.clientName == null) ||                                  // (Regular puck  OR
                (puck.clientName.slice(0,3) == 'NPC') ||                      //  Drone puck    OR
                ((puck.clientName == 'local') && (gW.dC.player.checked)) ) ) {   //  Local host and puck requested)
            
            // the remainder operator normalizes the angle to be within +/- 2Pi.
            if (puck.angle_r) puck.angle_r = puck.angle_r % (2 * Math.PI);
            
            var newPuck = new cP.Puck( puck.position_2d_m, puck.velocity_2d_mps, newBirth( puck, 'puck'));
            
            if (puck.jello) jM.addPuck( newPuck);
         }
      }
      
      // For the count-to-pi demos.
      if ( (typeof state_data.piCalcs !== "undefined") && 
           (['1.c','1.d','1.e'].includes( gW.demoVersionBase( gW.getDemoVersion()))) ) {
         
         gW.setPiCalcs( state_data.piCalcs.enabled, state_data.piCalcs.clacks, state_data.piCalcs.usePiEngine);
         
         if (state_data.piCalcs.usePiEngine) {
            if (state_data.piEngine) {
               cP.PiEngine.state                = state_data.piEngine;
               cP.PiEngine.state.collisionCount = state_data.piCalcs.collisionCount;
               cP.PiEngine.state.p1_v_max       = state_data.piCalcs.p1_v_max;
            }
         } else {
            // box2d engine
            gW.aT.puckMap['puck1'].vmax = state_data.piCalcs.p1_v_max;
            gW.aT.collisionCount        = state_data.piCalcs.collisionCount;         
         }
      }
      
      // get a reference to a table object using its name
      function tableObj( name) {
         let first3 = name.slice(0,3);
         let tableObj = null;
         if (first3 == "pin") {
            tableObj = gW.aT.pinMap[ name];
         } else if (first3 == "puc") {
            tableObj = gW.aT.puckMap[ name];
         } else if (first3 == "wal") {
            tableObj = gW.aT.wallMap[ name];
         }
         return tableObj;
      }
      
      // Rebuild the springs.
      for (var springName in state_data.springMapData) {
         var theSpring = state_data.springMapData[ springName];
         
         // Don't try to restore navigation springs. Those are created
         // when the NPC pucks are restored.
         if (!theSpring.navigationForNPC && !theSpring.forCursor) {
            let p1 = tableObj( theSpring.p1_name);
            let p2 = tableObj( theSpring.p2_name);
            
            if ((p1) && (p2)) {
               new cP.Spring(p1, p2, newBirth( theSpring, 's'));
            } else {
               console.log('WARNING: Attempting to rebuild a spring with one or both connected objects missing.');
            }
            
         }
      }
      // Rebuild the joints.
      for (var jointName in state_data.jointMapData) {
         var joint = state_data.jointMapData[ jointName];
         
         let to1 = tableObj( joint.jto1_name);
         let to2 = tableObj( joint.jto2_name);
         
         if ((to1) && (to2)) {
            new cP.Joint( to1, to2, newBirth( joint, 'j'));
         } else {
            console.log('WARNING: Attempting to rebuild a joint with one or both connected objects missing.');
         }
      }
      
      // Have this at the end because need the objects instantiated before setting the restitution values
      // in the pucks (side effect of setGravityRelatedParameters)
      gW.setG_ON( state_data.gravity);
      gW.dC.gravity.checked = gW.getG_ON();
      gW.setGravityRelatedParameters({});
      
      // Give priority to the host's puck for use as a template. If there was no host puck when
      // the capture was done, the network puck will be used.
      if (localHostPuckName) {
         puckNameForTemplate = localHostPuckName;
      } else {
         puckNameForTemplate = networkClientName;
      }
      
      // Sometimes just want to be sure the user gets the fullscreen view.
      if (state_data.fullScreenDemo) {
         gW.setFullScreenDemo(true);
      }
      
      // For example, the dandelion demos, turn shooter on for each client.
      if (state_data.lockedAndLoaded ) {
         gW.setLockedAndLoaded(true);
      }  
      
      // Exit here...
      if (puckNameForTemplate) {
         return state_data.puckMapData[ puckNameForTemplate];
      } else {
         // Looks like a capture was made after host and all network pucks were popped, savage battle.
         // So let's make a puck template from the default pars for the host puck.
         return Object.assign({}, {'position_2d_m':new cP.Vec2D(2.0, 2.0), 'velocity_2d_mps':new cP.Vec2D(0.0, 0.0)}, cP.Puck.hostPars);
      } 
   }
      
   // For loading and running a capture from a web page link.
   function demoStart_fromCapture( index, pars) {
      var fileName = setDefault( pars.fileName, 'null');
      console.log('fetching ' + fileName + ' from server');
      $.getScript( fileName, function() {
         // Note: demo_capture is a page level global and is assigned a value, the capture object, in the first line of the loading file.
         // Put the capture into the capture input box on the page.
         gW.dC.json.value = JSON.stringify( demo_capture, null, 3);
         window.setTimeout( function() { scrollCaptureArea();}, 500);
         gW.demoStart( index);
      }).fail( function() {
         // Try again...
         gW.messages['help'].newMessage("please wait...", 5.0);
         console.log('attempting second fetch ' + fileName + ' from server');
         $.getScript( fileName, function() {
            gW.dC.json.value = JSON.stringify( demo_capture, null, 3);
            window.setTimeout( function() { scrollCaptureArea();}, 500);
            gW.demoStart( index);

         }).fail( function() {
            console.log('capture file not found on server');
            gW.messages['help'].newMessage("Unable to get this capture file from the server: " + fileName + 
                                        "\\  please try again...", 10.0);
         });
      });
   }
   
   function scrollCaptureArea() {
      gW.dC.json.scrollTop = 30; 
      gW.dC.json.scrollLeft = 130;
   }
   
   
   // see comments before the "return" section of gwModule.js
   return {
      // Objects
      
      // Variables
      
      // Methods
      initializeModule: initializeModule,
      saveState: saveState,
      runCapture: runCapture,
      cleanCapture: cleanCapture,
      restoreFromState: restoreFromState,
      demoStart_fromCapture: demoStart_fromCapture,
      clearState: clearState,
      scrollCaptureArea: scrollCaptureArea
      
   };

})();