// Utilities () module
// utilities.js 
   console.log('UT version 0.0');
// 5:47 PM Mon May 16, 2022
// Written by: James D. Miller 

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

(function() {
   "use strict";
   
   // Short names for Box2D constructors and prototypes
   var b2Vec2 = Box2D.Common.Math.b2Vec2;  
   
   
   // Returns the default if the value is undefined.
   function setDefault( theValue, theDefault) {
      return (typeof theValue !== "undefined") ? theValue : theDefault;
   }
   
   
   // Relationships between the screen and the b2d world ///////////////////////
   
   // Scaler conversions
   function meters_from_px( length_px) {
      return length_px / gW.getPx_per_m();
   }
   
   function px_from_meters( length_m) {
      // Note: a call to the "math" library (in javascript) is quite slow.
      //return math.round( length_m * gW.getPx_per_m(), 2);
      // faster...
      //return parseFloat(  (length_m * gW.getPx_per_m()).toFixed(2)  );
      // even faster...
      //return Math.round( length_m * gW.getPx_per_m());
      // Fastest, just let it float. Browsers can handle fractional pixels.
      return length_m * gW.getPx_per_m();
   }

   // Vector conversions.
   function screenFromWorld( position_2d_m) {
      var x_px = px_from_meters( position_2d_m.x);
      var y_px = px_from_meters( position_2d_m.y);
      return new cP.Vec2D( x_px, gW.getCanvasDimensions().height - y_px);
   }
   
   // Translate back to raw screen coordinates from the coordinates of the imaging element.
   function rawScreenFromImagingElement( imagingElement, position_2d_px, border_px) {
      let x_raw_px, y_raw_px, imagingElementRect;
      if (gW.fullScreenState('get')) { 
         imagingElementRect = elementRectInFullScreen( imagingElement);
         x_raw_px = imagingElementRect.left + (position_2d_px.x * imagingElementRect.scaleFactor);
         y_raw_px = imagingElementRect.top  + (position_2d_px.y * imagingElementRect.scaleFactor);
         
      } else {
         imagingElementRect = imagingElement.getBoundingClientRect();
         x_raw_px = imagingElementRect.left + position_2d_px.x + border_px;
         y_raw_px = imagingElementRect.top  + position_2d_px.y + border_px;
      }
      return new cP.Vec2D( x_raw_px, y_raw_px);
   }
   
   function worldFromScreen( position_2d_px) {
      var x_m = meters_from_px( position_2d_px.x);
      var y_m = meters_from_px( gW.getCanvasDimensions().height - position_2d_px.y);
      return new cP.Vec2D( x_m, y_m); 
   }
 
   // Map (stretch) the raw touch-screen (mainly useful for cell phones) values out closer to the cushions in the pool table.
   function stretchRaw_px( raw_px, range_px, scaleFirstHalf, scaleSecondHalf) {
      var midpoint_px = range_px/2.0;
      if (raw_px < midpoint_px) {
         var stretched_px = midpoint_px - Math.abs(midpoint_px - raw_px) * scaleFirstHalf;
      } else {
         var stretched_px = midpoint_px + Math.abs(midpoint_px - raw_px) * scaleSecondHalf;
      }
      if (stretched_px < 0) stretched_px = 0;
      
      return stretched_px;
   }
 
   function elementRectInFullScreen( imagingElement) {
      var renderedElementRect = {};
      // The magic is in this next line. The image scaling is limited by the axis that is most fractionally similar to the corresponding view-port axis.
      // Scaling that axis until it matches the view-port avoids clipping the image along the other axis.
      // So, take the minimum of the two ratios. That's the limit for scaling without clipping.
      var widthRatio  = window.innerWidth / imagingElement.width;
      var heightRatio = window.innerHeight / imagingElement.height;
      renderedElementRect.whRatio = widthRatio / heightRatio;
      renderedElementRect.scaleFactor = Math.min( widthRatio, heightRatio);
      renderedElementRect.width  = imagingElement.width  * renderedElementRect.scaleFactor;
      renderedElementRect.height = imagingElement.height * renderedElementRect.scaleFactor;
      
      renderedElementRect.left = Math.max( (window.innerWidth  - renderedElementRect.width)/2, 0);
      renderedElementRect.top  = Math.max( (window.innerHeight - renderedElementRect.height)/2, 0);
      
      return renderedElementRect;
   }
 
   // Convert raw mouse value into the coordinates of the imaging element (iE), like the canvas for example.
   function screenFromRaw_2d_px( imagingElement, raw_2d_px, pars = {}) {
      var mouse_iE_2d_px = new cP.Vec2D(0, 0);
      var inputDevice = setDefault( pars.inputDevice, "mouse");  // default input device type
      var demoRunningOnHost = setDefault( pars.demoRunningOnHost, "N/A");
      var x_raw_px = raw_2d_px.x, x_px = x_raw_px;
      var y_raw_px = raw_2d_px.y, y_px = y_raw_px;
      var runningGhostBallPool = (demoRunningOnHost.slice(0,3) == "3.d");
      
      if (gW.fullScreenState('get')) { 
         var renderedElementRect = elementRectInFullScreen( imagingElement);
         
         // Stretch the raw cell-phone touchscreen input:
         // When playing pool on a cell-phone, it can be hard to touch the upper edge of the phone screen (landscape). This stretching also avoids a problem
         // on the left edge where dragging the ghost-ball over the left edge will trigger a release (a shot).
         if (runningGhostBallPool && (inputDevice == "touchScreen")) {
            // Bring left and right touch points in toward the middle. Again, this avoids accidentally touching controls or dragging off the left edge of the screen.
            var x_px = stretchRaw_px( x_raw_px, window.innerWidth,  1.15, 1.15);
            // When whRatio is greater than 1.0, the pool table fills the vertical range of the touch screen (landscape). Again, this can make
            // it difficult to reach the top and bottom cushions with thumb touches. In those cases, apply more stretch. This effectively 
            // brings the touch points in (a little) toward the middle of the touch screen.
            if (renderedElementRect.whRatio > 1.0) {
               var y_px = stretchRaw_px( y_raw_px, window.innerHeight, 1.20, 1.10);
            } else {
               var y_px = stretchRaw_px( y_raw_px, window.innerHeight, 1.10, 1.00);
            }
         }
         
         mouse_iE_2d_px.x = (x_px - renderedElementRect.left) / renderedElementRect.scaleFactor;
         mouse_iE_2d_px.y = (y_px - renderedElementRect.top) / renderedElementRect.scaleFactor;
         
      } else {
         var renderedElementRect = imagingElement.getBoundingClientRect();
         // Nudge it a little to account for the canvas border (5px when not fullscreen). This aligns our mouse tip with the Windows' mouse tip.
         mouse_iE_2d_px.x = x_raw_px - renderedElementRect.left - 5;
         mouse_iE_2d_px.y = y_raw_px - renderedElementRect.top  - 5; 
      }
      
      // This will help keep the ghost-ball from getting behind the cushions in the pool game.
      if (runningGhostBallPool) {
         // canvas width: 1915, height: 1075
         if (mouse_iE_2d_px.x < 5) {
            mouse_iE_2d_px.x = 5;
         } else if ((mouse_iE_2d_px.x > 1910)) {
            mouse_iE_2d_px.x = 1910;
         }
         if (mouse_iE_2d_px.y < 5) {
            mouse_iE_2d_px.y = 5;
         } else if ((mouse_iE_2d_px.y > 1070)) {
            mouse_iE_2d_px.y = 1070;
         }
      }
      /*
      var debugString = "yR:"  + Math.round( y_raw_px) + ",yS:" + Math.round( y_px) + ", xR:" + Math.round( x_raw_px) + ",xS:" + Math.round( x_px) +   "     xF:" + mouse_iE_2d_px.x.toFixed(1) + ",yF:" + mouse_iE_2d_px.y.toFixed(1) + 
                        "\\wW:" + window.innerWidth + ",wH:" + window.innerHeight + ", eW:" + Math.round( imagingElement.width)   + ",eH:" + Math.round( imagingElement.height) + ", whR:" + renderedElementRect.whRatio.toFixed(3) +
                        ", inputDevice=" + inputDevice;
      // This will display for the host's mouse and touch events if the host is connected to the server.
      hC.sendSocketControlMessage( {'from':'anyone', 'to':'host', 'data':{'androidDebug':{'value':true,'debugString':debugString}} } );
      */
      return mouse_iE_2d_px; // in coordinates of the imaging element (iE)
   }
   
   function fineMoves( clientName, posOnCanvas_2d_px) {
      /*
      There are several lines of code here where the result in this frame is needed for processing the next frame.
      These results are vector objects and so some consideration should be given to the fact that a reference to these object results
      does not make a copy, just a reference. And so it might be clearer here to use the Vec2D copy method to avoid the 
      reference behavior. However, all these cursor related results are regenerated (instantiated) each frame and so that breaks the
      the reference. So, a little faster to not use Vec2D copy here.
      */
      var fineAdjust_2d_px;
      var client = gW.clients[ clientName];
      
      if (client.fineMovesState == 'on') {
         var diff_2d_px = posOnCanvas_2d_px.subtract( client.prevNormalCursorPosOnCanvas_2d_px).scaleBy(0.15);
         fineAdjust_2d_px = client.previousFine_2d_px.add( diff_2d_px);
      
         // Save the fine-adjust result.
         client.previousFine_2d_px = fineAdjust_2d_px;
      
      } else if (client.fineMovesState == 'startTransition') {
         client.fineMovesState = 'inTransition';
         var transitionSteps = 10; 
         var transitionStepWait_ms = gW.getDeltaT_s() * 1000.0; // one step per frame
         
         setTimeout(function runTransiton() {
            client.fMT.count++;
            /*
            For example, if the initial separation is 22 parts, the following transition series results in equal intervals as follows:
            factor:              1/11,    1/10,    1/9, ...   1/2 
            difference:       22/11=2, 20/10=2, 18/9=2, ... 4/2=2 
            decreasing separation: 22,      20,     18, ...     2 
            This recursive transition always uses the current mouse position, so even if the mouse is moving, this closes in on the mouse position.
            */
            var reductionFraction = 1.0/((transitionSteps + 2) - client.fMT.count);
            
            // difference between center of ghost ball (location of client pin) and the actual cursor position (previous value). 
            var diff_2d_px = client.pin.position_2d_px.subtract( client.prevNormalCursorPosOnCanvas_2d_px).scaleBy( reductionFraction);
            
            fineAdjust_2d_px = client.previousFine_2d_px.subtract( diff_2d_px);
            
            // Save the fine-adjust result.
            client.previousFine_2d_px = fineAdjust_2d_px;
            
            client.mouse_async_2d_px = fineAdjust_2d_px;
            
            if (client.fMT.count <= transitionSteps) {
               // Recursive...
               setTimeout( runTransiton, transitionStepWait_ms);
               
            } else {
               client.fMT.count = 0;
               client.fineMovesState = 'off'
            }
            
         }, transitionStepWait_ms);  
      
      } else if (client.fineMovesState == 'off') {
         // do nothing; no high-resolution movement
         fineAdjust_2d_px = posOnCanvas_2d_px;
      }
      
      //var debugString = "fine:" + math.round(fineAdjust_2d_px.x,2) + "," + math.round(fineAdjust_2d_px.y,2) + ", reg:" + posOnCanvas_2d_px.x + "," + posOnCanvas_2d_px.y;
      //hC.sendSocketControlMessage( {'from':'anyone', 'to':'host', 'data':{'androidDebug':{'value':true,'debugString':debugString}} } );
      
      // Save the incoming (primary) cursor position on the canvas.
      client.prevNormalCursorPosOnCanvas_2d_px = posOnCanvas_2d_px;
      
      return fineAdjust_2d_px;
   }
 
   // Functions to convert between vector types
   function Vec2D_from_b2Vec2( b2Vector) {
      return new cP.Vec2D( b2Vector.x, b2Vector.y);
   }
   function b2Vec2_from_Vec2D( vec2D) {
      return new b2Vec2( vec2D.x, vec2D.y);
   }
   
   // This check is useful to prevent problems (objects stripped of their methods) when reconstructing from a 
   // JSON capture.
   function Vec2D_check( vector_2d) {
      if (vector_2d.constructor.name == "Vec2D") {
         return vector_2d;
      } else {
         return new cP.Vec2D( vector_2d.x, vector_2d.y);
      }
   }
 
   
   // Reveal these methods to the global windows scope.
   window.setDefault = setDefault;
   
   window.worldFromScreen = worldFromScreen;
   window.fineMoves = fineMoves;
   window.screenFromWorld = screenFromWorld;
   window.rawScreenFromImagingElement = rawScreenFromImagingElement;
   window.screenFromRaw_2d_px = screenFromRaw_2d_px;
   window.Vec2D_from_b2Vec2 = Vec2D_from_b2Vec2;
   window.b2Vec2_from_Vec2D = b2Vec2_from_Vec2D;
   window.px_from_meters = px_from_meters;
   window.meters_from_px = meters_from_px;
   window.Vec2D_check = Vec2D_check;
   
})();