// Utilities () module
// utilities.js 
   console.log('UT version 0.0');
// 3:55 PM Wed August 10, 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;
   }
 
   // A check to see if the cursor position is over the canvas (or other specified element).
   function mouseOverElement( imagingElement, raw_2d_px) {
      var renderedElementRect = imagingElement.getBoundingClientRect();
      let withinXrange = (raw_2d_px.x >= renderedElementRect.left) && (raw_2d_px.x <= renderedElementRect.right);
      let withinYrange = (raw_2d_px.y >= renderedElementRect.top)  && (raw_2d_px.y <= renderedElementRect.bottom);
      let overElement = (withinXrange && withinYrange);
      return overElement;
   }
 
   // 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;
      var x_px = x_raw_px;
      var y_raw_px = raw_2d_px.y; 
      var y_px = y_raw_px;
      var runningGhostBallPool = (demoRunningOnHost.slice(0,3) == "3.d");
      let projectileGames = ['4.e','5.e'].includes( demoRunningOnHost.slice(0,3));
      
      if (gW.fullScreenState('get')) { 
         var renderedElementRect = elementRectInFullScreen( imagingElement);
         /*
         let debugString = "whRatio=" + renderedElementRect.whRatio;
         hC.sendSocketControlMessage( {'from':'anyone', 'to':'host', 'data':{'androidDebug':{'value':true,'debugString':debugString}} } );
         */
         // 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 (inputDevice == "touchScreen") {
            if (runningGhostBallPool) {
               // 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.
               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) {
                  // cell phone, my pixel is about 1.22
                  y_px = stretchRaw_px( y_raw_px, window.innerHeight, 1.20, 1.10);
               } else {
                  // laptop, 0.999
                  y_px = stretchRaw_px( y_raw_px, window.innerHeight, 1.10, 1.00);
               }
               
            } else if (projectileGames) {
               if (renderedElementRect.whRatio > 1.0) {
                  // cell phone
                  x_px = stretchRaw_px( x_raw_px, window.innerWidth,  0.95, 0.95);
                  y_px = stretchRaw_px( y_raw_px, window.innerHeight, 1.20, 1.30);
               } else {
                  // laptop
                  x_px = stretchRaw_px( x_raw_px, window.innerWidth,  1.10, 1.10);
                  y_px = stretchRaw_px( y_raw_px, window.innerHeight, 1.20, 1.30);
               }
            }
         }
         
         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 exitFineMoves( clientName) {
      // This is a recursive transition that brings the finemoves cursor back to the main cursor position.
      var fineAdjust_2d_px;
      var client = gW.clients[ clientName];
      
      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);
   }
   
   function fineMoves( clientName, posOnCanvas_2d_px) {
      /*
      Note: these cursor related results associated with fineMoves are regenerated (instantiated) each frame 
      and so that breaks any frame-to-frame references to these vectors (objects).
      */
      var fineAdjust_2d_px;
      var client = gW.clients[ clientName];
      
      if (client.fineMovesState == 'on') {
         // Move 15% of the normal 1-to-1 movement.
         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 == '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.exitFineMoves = exitFineMoves;
   window.screenFromWorld = screenFromWorld;
   window.mouseOverElement = mouseOverElement;
   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;
   
})();