text: //
  //  Created by Ryan Huffman on 1/10/2017
  //  Copyright 2017 High Fidelity, Inc.
  //
  //  Distributed under the Apache License, Version 2.0.
  //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
  //
  
  /* globals ShortbowGameManager:true, utils */
  
  Script.include('utils.js');
  
  // +--------+      +-----------+      +-----------------+
  // |        |      |           |<-----+                 |
  // |  IDLE  +----->|  PLAYING  |      |  BETWEEN_WAVES  |
  // |        |      |           +----->|                 |
  // +--------+      +-----+-----+      +-----------------+
  //      ^                |
  //      |                v
  //      |         +-------------+
  //      |         |             |
  //      +---------+  GAME_OVER  |
  //                |             |
  //                +-------------+
  var GAME_STATES = {
      IDLE: 0,
      PLAYING: 1,
      BETWEEN_WAVES: 2,
      GAME_OVER: 3
  };
  
  // Load the sounds that we will be using in the game so they are ready to be
  // used when we need them.
  var BEGIN_BUILDING_SOUND = SoundCache.getSound(Script.resolvePath("sounds/gameOn.wav"));
  var GAME_OVER_SOUND = SoundCache.getSound(Script.resolvePath("sounds/gameOver.wav"));
  var WAVE_COMPLETE_SOUND = SoundCache.getSound(Script.resolvePath("sounds/waveComplete.wav"));
  var EXPLOSION_SOUND = SoundCache.getSound(Script.resolvePath("sounds/explosion.wav"));
  var TARGET_HIT_SOUND = SoundCache.getSound(Script.resolvePath("sounds/targetHit.wav"));
  var ESCAPE_SOUND = SoundCache.getSound(Script.resolvePath("sounds/escape.wav"));
  
  const STARTING_NUMBER_OF_LIVES = 6;
  const ENEMIES_PER_WAVE_MULTIPLIER = 2;
  const POINTS_PER_KILL = 100;
  const ENEMY_SPEED = 3.0;
  
  // Encode a set of key-value pairs into a param string. Does NOT do any URL escaping.
  function encodeURLParams(params) {
      var paramPairs = [];
      for (var key in params) {
          paramPairs.push(key + "=" + params[key]);
      }
      return paramPairs.join("&");
  }
  
  function sendAndUpdateHighScore(entityID, score, wave, numPlayers, onResposeReceived) {
      const URL = 'https://script.google.com/macros/s/AKfycbwbjCm9mGd1d5BzfAHmVT_XKmWyUYRkjCEqDOKm1368oM8nqWni/exec';
      print("Sending high score");
  
      const paramString = encodeURLParams({
          entityID: entityID,
          score: score,
          wave: wave,
          numPlayers: numPlayers
      });
  
      var request = new XMLHttpRequest();
      request.onreadystatechange = function() {
          print("ready state: ", request.readyState, request.status, request.readyState === request.DONE, request.response);
          if (request.readyState === request.DONE && request.status === 200) {
              print("Got response for high score: ", request.response);
              var response = JSON.parse(request.responseText);
              if (response.highScore !== undefined) {
                  onResposeReceived(response.highScore);
              }
          }
      };
      request.open('GET', URL + "?" + paramString);
      request.timeout = 10000;
      request.send();
  }
  
  function findChildrenWithName(parentID, name) {
      var childrenIDs = Entities.getChildrenIDs(parentID);
      var matchingIDs = [];
      for (var i = 0; i < childrenIDs.length; ++i) {
          var id = childrenIDs[i];
          var childName = Entities.getEntityProperties(id, 'name').name;
          if (childName === name) {
              matchingIDs.push(id);
          }
      }
      return matchingIDs;
  }
  
  function getPropertiesForEntities(entityIDs, desiredProperties) {
      var properties = [];
      for (var i = 0; i < entityIDs.length; ++i) {
          properties.push(Entities.getEntityProperties(entityIDs[i], desiredProperties));
      }
      return properties;
  }
  
  
  var baseEnemyProperties = {
      "name": "SB.Enemy",
      "damping": 0,
      "linearDamping": 0,
      "angularDamping": 0,
      "acceleration": {
          "x": 0,
          "y": -9,
          "z": 0
      },
      "angularVelocity": {
          "x": -0.058330666273832321,
          "y": -0.77943277359008789,
          "z": -2.1163818836212158
      },
      "clientOnly": 0,
      "collisionsWillMove": 1,
      "dimensions": {
          "x": 0.63503998517990112,
          "y": 0.63503998517990112,
          "z": 0.63503998517990112
      },
      "dynamic": 1,
      "gravity": {
          "x": 0,
          "y": -15,
          "z": 0
      },
      "lifetime": 30,
      "id": "{ed8f7339-8bbd-4750-968e-c3ceb9d64721}",
      "modelURL": Script.resolvePath("models/Amber.baked.fbx"),
      "owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
      "queryAACube": {
          "scale": 1.0999215841293335,
          "x": -0.54996079206466675,
          "y": -0.54996079206466675,
          "z": -0.54996079206466675
      },
      "shapeType": "sphere",
      "type": "Model",
      "script": Script.resolvePath('enemyClientEntity.js'),
      "serverScripts": Script.resolvePath('enemyServerEntity.js')
  };
  
  function searchForChildren(parentID, names, callback, timeoutMs) {
      // Map from name to entity ID for the children that have been found
      var foundEntities = {};
      for (var i = 0; i < names.length; ++i) {
          foundEntities[names[i]] = null;
      }
  
      const CHECK_EVERY_MS = 500;
      const maxChecks = Math.ceil(timeoutMs / CHECK_EVERY_MS);
  
      var check = 0;
      var intervalID = Script.setInterval(function() {
          check++;
  
          var childrenIDs = Entities.getChildrenIDs(parentID);
          print("\tNumber of children:", childrenIDs.length);
  
          for (var i = 0; i < childrenIDs.length; ++i) {
              print("\t\t" + i + ".", Entities.getEntityProperties(childrenIDs[i]).name);
              var id = childrenIDs[i];
              var name = Entities.getEntityProperties(id, 'name').name;
              var idx = names.indexOf(name);
              if (idx > -1) {
                  foundEntities[name] = id;
                  print(name, id);
                  names.splice(idx, 1);
              }
          }
  
          if (names.length === 0 || check >= maxChecks) {
              Script.clearInterval(intervalID);
              callback(foundEntities);
          }
      }, CHECK_EVERY_MS);
  }
  
  ShortbowGameManager = function(rootEntityID, bowPositions, spawnPositions) {
      print("Starting game manager");
      var self = this;
  
      this.gameState = GAME_STATES.IDLE;
  
      this.rootEntityID = rootEntityID;
      this.bowPositions = bowPositions;
      this.rootPosition = null;
      this.spawnPositions = spawnPositions;
  
      this.loadedChildren = false;
  
      const START_BUTTON_NAME = 'SB.StartButton';
      const WAVE_DISPLAY_NAME = 'SB.DisplayWave';
      const SCORE_DISPLAY_NAME = 'SB.DisplayScore';
      const LIVES_DISPLAY_NAME = 'SB.DisplayLives';
      const HIGH_SCORE_DISPLAY_NAME = 'SB.DisplayHighScore';
  
      const SEARCH_FOR_CHILDREN_TIMEOUT = 5000;
  
      searchForChildren(rootEntityID, [
          START_BUTTON_NAME,
          WAVE_DISPLAY_NAME,
          SCORE_DISPLAY_NAME,
          LIVES_DISPLAY_NAME,
          HIGH_SCORE_DISPLAY_NAME
      ], function(children) {
          self.loadedChildren = true;
          self.startButtonID = children[START_BUTTON_NAME];
          self.waveDisplayID = children[WAVE_DISPLAY_NAME];
          self.scoreDisplayID = children[SCORE_DISPLAY_NAME];
          self.livesDisplayID = children[LIVES_DISPLAY_NAME];
          self.highScoreDisplayID = children[HIGH_SCORE_DISPLAY_NAME];
  
          sendAndUpdateHighScore(self.rootEntityID, self.score, self.waveNumber, 1, self.setHighScore.bind(self));
  
          self.reset();
      }, SEARCH_FOR_CHILDREN_TIMEOUT);
  
      // Gameplay state
      this.waveNumber = 0;
      this.livesLeft = STARTING_NUMBER_OF_LIVES;
      this.score = 0;
      this.nextWaveTimer = null;
      this.spawnEnemyTimers = [];
      this.remainingEnemies = [];
      this.bowIDs = [];
  
      this.startButtonChannelName = 'button-' + this.rootEntityID;
  
      // Entity client and server scripts will send messages to this channel
      this.commChannelName = "shortbow-" + this.rootEntityID;
      Messages.subscribe(this.commChannelName);
      Messages.messageReceived.connect(this, this.onReceivedMessage);
      print("Listening on: ", this.commChannelName);
      Messages.sendMessage(this.commChannelName, 'hi');
  };
  ShortbowGameManager.prototype = {
      reset: function() {
          Entities.editEntity(this.startButtonID, { visible: true });
      },
      cleanup: function() {
          Messages.unsubscribe(this.commChannelName);
          Messages.messageReceived.disconnect(this, this.onReceivedMessage);
  
          if (this.checkEnemiesTimer) {
              Script.clearInterval(this.checkEnemiesTimer);
              this.checkEnemiesTimer = null;
          }
  
          for (var i = this.bowIDs.length - 1; i >= 0; i--) {
              Entities.deleteEntity(this.bowIDs[i]);
          }
          this.bowIDs = [];
          for (i = 0; i < this.remainingEnemies.length; i++) {
              Entities.deleteEntity(this.remainingEnemies[i].id);
          }
          this.remainingEnemies = [];
  
          this.gameState = GAME_STATES.IDLE;
      },
      startGame: function() {
          if (this.gameState !== GAME_STATES.IDLE) {
              print("shortbowGameManagerManager.js | Error, trying to start game when not in idle state");
              return;
          }
  
          if (this.loadedChildren === false) {
              print('shortbowGameManager.js | Children have not loaded, not allowing game to start');
              return;
          }
  
          print("Game started!!");
  
          this.rootPosition = Entities.getEntityProperties(this.rootEntityID, 'position').position;
  
          Entities.editEntity(this.startButtonID, { visible: false });
  
          // Spawn bows
          var bowSpawnEntityIDs = findChildrenWithName(this.rootEntityID, 'SB.BowSpawn');
          var bowSpawnProperties = getPropertiesForEntities(bowSpawnEntityIDs, ['position', 'rotation']);
          for (var i = 0; i < bowSpawnProperties.length; ++i) {
              const props = bowSpawnProperties[i];
              Vec3.print("Creating bow: " + i, props.position);
              this.bowIDs.push(Entities.addEntity({
                  "position": props.position,
                  "rotation": props.rotation,
                  "collisionsWillMove": 1,
                  "compoundShapeURL": Script.resolvePath("bow/models/bow_collision_hull.obj"),
                  "created": "2016-09-01T23:57:55Z",
                  "dimensions": {
                      "x": 0.039999999105930328,
                      "y": 1.2999999523162842,
                      "z": 0.20000000298023224
                  },
                  "dynamic": 1,
                  "gravity": {
                      "x": 0,
                      "y": -9.8,
                      "z": 0
                  },
                  "modelURL": Script.resolvePath("bow/models/bow-deadly.baked.fbx"),
                  "name": "WG.Hifi-Bow",
                  "script": Script.resolvePath("bow/bow.js"),
                  "shapeType": "compound",
                  "type": "Model",
                  "userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}"
              }));
          }
  
          // Initialize game state
          this.waveNumber = 0;
          this.setScore(0);
          this.setLivesLeft(STARTING_NUMBER_OF_LIVES);
  
          this.nextWaveTimer = Script.setTimeout(this.startNextWave.bind(this), 100);
          this.spawnEnemyTimers = [];
          this.checkEnemiesTimer = null;
          this.remainingEnemies = [];
  
          // SpawnQueue is a list of enemies left to spawn. Each entry looks like:
          //
          //   { spawnAt: 1000, position: { x: 0, y: 0, z: 0 } }
          //
          // where spawnAt is the number of millseconds after the start of the wave
          // to spawn the enemy. The list is sorted by spawnAt, ascending.
          this.spawnQueue = [];
  
          this.gameState = GAME_STATES.BETWEEN_WAVES;
  
          Audio.playSound(BEGIN_BUILDING_SOUND, {
              volume: 1.0,
              position: this.rootPosition
          });
  		
  		var liveChecker = setInterval(function() {
  			if (this.livesLeft <= 0) {
  				this.endGame();
  				clearInterval(liveChecker);
  			}
  		}, 1000);
      },
      startNextWave: function() {
          if (this.gameState !== GAME_STATES.BETWEEN_WAVES) {
              return;
          }
  
          print("Starting next wave");
          this.gameState = GAME_STATES.PLAYING;
          this.waveNumber++;
          this.remainingEnemies= [];
          this.spawnQueue = [];
          this.spawnStartTime = Date.now();
  
          Entities.editEntity(this.waveDisplayID, { text: this.waveNumber});
  
          var numberOfEnemiesLeftToSpawn = this.waveNumber * ENEMIES_PER_WAVE_MULTIPLIER;
          var delayBetweenSpawns = 2000 / Math.max(1, Math.log(this.waveNumber));
          var currentDelay = 2000;
  
          print("Number of enemies:", numberOfEnemiesLeftToSpawn);
          this.checkEnemiesTimer = Script.setInterval(this.checkEnemies.bind(this), 100);
  
          var enemySpawnEntityIDs = findChildrenWithName(this.rootEntityID, 'SB.EnemySpawn');
          var enemySpawnProperties = getPropertiesForEntities(enemySpawnEntityIDs, ['position', 'rotation']);
  
          for (var i = 0; i < numberOfEnemiesLeftToSpawn; ++i) {
              print("Adding enemy");
              var idx = Math.floor(Math.random() * enemySpawnProperties.length);
              var props = enemySpawnProperties[idx];
              this.spawnQueue.push({
                  spawnAt: currentDelay,
                  position: props.position,
                  rotation: props.rotation,
                  velocity: Vec3.multiply(ENEMY_SPEED, Quat.getFront(props.rotation))
  
              });
              currentDelay += delayBetweenSpawns;
          }
  
          print("Starting wave", this.waveNumber);
  
      },
      checkWaveComplete: function() {
          if (this.gameState !== GAME_STATES.PLAYING) {
              return;
          }
  
          if (this.spawnQueue.length === 0 && this.remainingEnemies.length === 0) {
              this.gameState = GAME_STATES.BETWEEN_WAVES;
              Script.setTimeout(this.startNextWave.bind(this), 5000);
  
              Script.clearInterval(this.checkEnemiesTimer);
              this.checkEnemiesTimer = null;
  
              // Play after 1.5s to let other sounds finish playing
              var self = this;
              Script.setTimeout(function() {
                  Audio.playSound(WAVE_COMPLETE_SOUND, {
                      volume: 1.0,
                      position: self.rootPosition
                  });
              }, 1500);
          }
      },
      setHighScore: function(highScore) {
          print("Setting high score to:", this.highScoreDisplayID, highScore);
          Entities.editEntity(this.highScoreDisplayID, { text: highScore });
      },
      setLivesLeft: function(lives) {
          lives = Math.max(0, lives);
          this.livesLeft = lives;
          Entities.editEntity(this.livesDisplayID, { text: this.livesLeft });
      },
      setScore: function(score) {
          this.score = score;
          Entities.editEntity(this.scoreDisplayID, { text: this.score });
      },
      checkEnemies: function() {
          if (this.gameState !== GAME_STATES.PLAYING) {
              return;
          }
  
          // Check the spawn queueu to see if there are any enemies that need to
          // be spawned
          var waveElapsedTime = Date.now() - this.spawnStartTime;
          while (this.spawnQueue.length > 0 && waveElapsedTime > this.spawnQueue[0].spawnAt) {
              baseEnemyProperties.position = this.spawnQueue[0].position;
              baseEnemyProperties.rotation = this.spawnQueue[0].rotation;
              baseEnemyProperties.velocity= this.spawnQueue[0].velocity;
  
              baseEnemyProperties.userData = JSON.stringify({
                  gameChannel: this.commChannelName,
                  grabbableKey: {
                      grabbable: false
                  }
              });
  
              var entityID = Entities.addEntity(baseEnemyProperties);
              this.remainingEnemies.push({
                  id: entityID,
                  lastKnownPosition: baseEnemyProperties.position,
                  lastHeartbeat: Date.now()
              });
              this.spawnQueue.splice(0, 1);
              Script.setTimeout(function() {
                  const JUMP_SPEED = 5.0;
                  var velocity = Entities.getEntityProperties(entityID, 'velocity').velocity;
                  velocity.y += JUMP_SPEED;
                  Entities.editEntity(entityID, { velocity: velocity });
  
              }, 500 + Math.random() * 4000);
          }
  
          // Check the list of remaining enemies to see if any are too far away
          // or haven't been heard from in awhile - if so, delete them.
          var enemiesEscaped = false;
          const MAX_UNHEARD_TIME_BEFORE_DESTROYING_ENTITY_MS = 5000;
          const MAX_DISTANCE_FROM_GAME_BEFORE_DESTROYING_ENTITY = 200;
          for (var i = this.remainingEnemies.length - 1; i >= 0; --i) {
              var enemy = this.remainingEnemies[i];
              var timeSinceLastHeartbeat = Date.now() - enemy.lastHeartbeat;
              var distance = Vec3.distance(enemy.lastKnownPosition, this.rootPosition);
              if (timeSinceLastHeartbeat > MAX_UNHEARD_TIME_BEFORE_DESTROYING_ENTITY_MS
                  || distance > MAX_DISTANCE_FROM_GAME_BEFORE_DESTROYING_ENTITY) {
  
                  print("EXPIRING: ", enemy.id);
                  Entities.deleteEntity(enemy.id);
                  this.remainingEnemies.splice(i, 1);
  				// Play the sound when you hit an enemy
                  Audio.playSound(TARGET_HIT_SOUND, {
                      volume: 1.0,
                      position: this.rootPosition
                  });
                  this.setScore(this.score + POINTS_PER_KILL);
                  enemiesEscaped = true;
              }
          }
  
          if (enemiesEscaped) {
              this.checkWaveComplete();
          }
      },
      endGame: function() {
          if (this.gameState !== GAME_STATES.PLAYING) {
              return;
          }
  
          var self = this;
          Script.setTimeout(function() {
              Audio.playSound(GAME_OVER_SOUND, {
                  volume: 1.0,
                  position: self.rootPosition
              });
          }, 1500);
  
          this.gameState = GAME_STATES.GAME_OVER;
          print("GAME OVER");
  
          // Update high score
          sendAndUpdateHighScore(this.rootEntityID, this.score, this.waveNumber, 1, this.setHighScore.bind(this));
  
          // Cleanup
          Script.clearTimeout(this.nextWaveTimer);
          this.nextWaveTimer = null;
          var i;
          for (i = 0; i < this.spawnEnemyTimers.length; ++i) {
              Script.clearTimeout(this.spawnEnemyTimers[i]);
          }
          this.spawnEnemyTimers = [];
  
          Script.clearInterval(this.checkEnemiesTimer);
          this.checkEnemiesTimer = null;
  
  
          for (i = this.bowIDs.length - 1; i >= 0; i--) {
              var id = this.bowIDs[i];
              print("Checking bow: ", id);
              var userData = utils.parseJSON(Entities.getEntityProperties(id, 'userData').userData);
              var bowIsHeld = userData.grabKey !== undefined && userData.grabKey !== undefined && userData.grabKey.refCount > 0;
              print("Held: ", bowIsHeld);
              if (!bowIsHeld) {
                  Entities.deleteEntity(id);
                  this.bowIDs.splice(i, 1);
              }
          }
  
          for (i = 0; i < this.remainingEnemies.length; i++) {
              Entities.deleteEntity(this.remainingEnemies[i].id);
          }
          this.remainingEnemies = [];
  
          // Wait a short time before showing the start button so that any current sounds
          // can finish playing.
          const WAIT_TO_REENABLE_GAME_TIMEOUT_MS = 3000;
          Script.setTimeout(function() {
              Entities.editEntity(this.startButtonID, { visible: true });
              this.gameState = GAME_STATES.IDLE;
          }.bind(this), WAIT_TO_REENABLE_GAME_TIMEOUT_MS);
      },
      onReceivedMessage: function(channel, messageJSON, senderID) {
          if (channel === this.commChannelName) {
              var message = utils.parseJSON(messageJSON);
              if (message === undefined) {
                  print("shortbowGameManager.js | Received non-json message:", JSON.stringify(messageJSON));
                  return;
              }
              switch (message.type) {
                  case 'start-game':
                      this.startGame();
                      break;
                  case 'enemy-killed':
                      this.onEnemyKilled(message.entityID, message.position);
                      break;
                  case 'enemy-escaped':
                      this.onEnemyEscaped(message.entityID);
                      break;
                  case 'enemy-heartbeat':
                      this.onEnemyHeartbeat(message.entityID, message.position);
                      break;
                  default:
                      print("shortbowGameManager.js | Ignoring unknown message type: ", message.type);
                      break;
              }
          }
      },
      onEnemyKilled: function(entityID, position) {
  		if (this.gameState !== GAME_STATES.PLAYING) {
              return;
          }
  		
          for (var i = this.remainingEnemies.length - 1; i >= 0; --i) {
              var enemy = this.remainingEnemies[i];
              if (enemy.id === entityID) {
                  this.remainingEnemies.splice(i, 1);
                  Audio.playSound(TARGET_HIT_SOUND, {
                      volume: 1.0,
                      position: this.rootPosition
                  });
                  // Update score
                  this.setScore(this.score + POINTS_PER_KILL);
                  print("SCORE: ", this.score);
  
                  this.checkWaveComplete();
                  break;
              }
          }
      },
      onEnemyEscaped: function(entityID, position) {
          if (this.gameState !== GAME_STATES.PLAYING) {
              return;
          }
  
          var enemiesEscaped = false;
          for (var i = this.remainingEnemies.length - 1; i >= 0; --i) {
              var enemy = this.remainingEnemies[i];
              if (enemy.id === entityID) {
  				
                  Entities.deleteEntity(enemy.id);
                  this.remainingEnemies.splice(i, 1);
                  this.setLivesLeft(this.livesLeft - 1);
                  Audio.playSound(ESCAPE_SOUND, {
                      volume: 1.0,
                      position: this.rootPosition
                  });
                  enemiesEscaped = true;
              }
          }
          if (this.livesLeft <= 0) {
              this.endGame();
          } else if (enemiesEscaped) {
              this.checkWaveComplete();
          }
      },
      onEnemyHeartbeat: function(entityID, position) {
          for (var i = 0; i < this.remainingEnemies.length; i++) {
              var enemy = this.remainingEnemies[i];
              if (enemy.id === entityID) {
                  enemy.lastHeartbeat = Date.now();
                  enemy.lastKnownPosition = position;
                  break;
              }
          }
      }
  };
  