/*
Copyright 2022 James D. Miller

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

// Utilities () module
// utilities.js 
   console.log('UT version 0.0');
// 3:55 PM Wed August 10, 2022

/*
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;
   
})();