text: //
  //  notifications.js
  //  Version 0.801
  //  Created by Adrian
  //
  //  Adrian McCarlie 8-10-14
  //  This script demonstrates on-screen overlay type notifications.
  //  Copyright 2014 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
  
  //  This script generates notifications created via a number of ways, such as:
  //  keystroke:
  //
  //  CTRL/s for snapshot.
  //  CTRL/m for mic mute and unmute.
  
  //  System generated notifications:
  //  If Screen is resized.
  //  If mic is muted for any reason.
  //
  //  To add a new System notification type:
  //
  //  1. Set the Event Connector at the bottom of the script.
  //  example:
  //  AudioDevice.muteToggled.connect(onMuteStateChanged);
  //
  //  2. Create a new function to produce a text string, do not include new line returns.
  //  example:
  //  function onMuteStateChanged() {
  //     var muteState,
  //         muteString;
  //
  //     muteState = AudioDevice.getMuted() ? "muted" : "unmuted";
  //     muteString = "Microphone is now " + muteState;
  //     createNotification(muteString, NotificationType.MUTE_TOGGLE);
  //  }
  //
  //  This new function must call wordWrap(text) if the length of message is longer than 42 chars or unknown.
  //  wordWrap() will format the text to fit the notifications overlay and return it
  //  after that we will send it to createNotification(text).
  //  If the message is 42 chars or less you should bypass wordWrap() and call createNotification() directly.
  
  //  To add a keypress driven notification:
  //
  //  1. Add a key to the keyPressEvent(key).
  //  2. Declare a text string.
  //  3. Call createNotifications(text, NotificationType) parsing the text.
  //  example:
  //  if (key.text === "s") {
  //      if (ctrlIsPressed === true) {
  //          noteString = "Snapshot taken.";
  //          createNotification(noteString, NotificationType.SNAPSHOT);
  //      }
  //  }
  Script.include("./libraries/globals.js");
  Script.include("./libraries/soundArray.js");
  
  var width = 340.0; //width of notification overlay
  var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window
  var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window
  var buttonLocationX = overlayLocationX + (width - 28.0);
  var locationY = 20.0; // position down from top of interface window
  var topMargin = 13.0;
  var leftMargin = 10.0;
  var textColor =  { red: 228, green: 228, blue: 228}; // text color
  var backColor =  { red: 2, green: 2, blue: 2}; // background color was 38,38,38
  var backgroundAlpha = 0;
  var fontSize = 12.0;
  var PERSIST_TIME_2D = 10.0;  // Time in seconds before notification fades
  var PERSIST_TIME_3D = 15.0;
  var persistTime = PERSIST_TIME_2D;
  var clickedText = false;
  var frame = 0;
  var ourWidth = Window.innerWidth;
  var ourHeight = Window.innerHeight;
  var text = "placeholder";
  var ctrlIsPressed = false;
  var ready = true;
  var MENU_NAME = 'Tools > Notifications';
  var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds";
  var NOTIFICATION_MENU_ITEM_POST = " Notifications";
  var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds";
  var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_";
  var lodTextID = false;
  
  var NotificationType = {
      UNKNOWN: 0,
      MUTE_TOGGLE: 1,
      SNAPSHOT: 2,
      WINDOW_RESIZE: 3,
      LOD_WARNING: 4,
      CONNECTION_REFUSED: 5,
      properties: [
          { text: "Mute Toggle" },
          { text: "Snapshot" },
          { text: "Window Resize" },
          { text: "Level of Detail" },
          { text: "Connection Refused" }
      ],
      getTypeFromMenuItem: function(menuItemName) {
          if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) {
              return NotificationType.UNKNOWN;
          }
          var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length);
          for (type in this.properties) {
              if (this.properties[type].text === preMenuItemName) {
                  return parseInt(type) + 1;
              }
          }
          return NotificationType.UNKNOWN;
      },
      getMenuString: function(type) {
          return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST;
      }
  };
  
  var randomSounds = new SoundArray({ localOnly: true }, true);
  var numberOfSounds = 2;
  for (var i = 1; i <= numberOfSounds; i++) {
      randomSounds.addSound(HIFI_PUBLIC_BUCKET + "sounds/UI/notification-general" + i + ".raw");
  }
  
  var notifications = [];
  var buttons = [];
  var times = [];
  var heights = [];
  var myAlpha = [];
  var arrays = [];
  var isOnHMD = false,
      NOTIFICATIONS_3D_DIRECTION = 0.0,  // Degrees from avatar orientation.
      NOTIFICATIONS_3D_DISTANCE = 0.6,  // Horizontal distance from avatar position.
      NOTIFICATIONS_3D_ELEVATION = -0.8,  // Height of top middle of top notification relative to avatar eyes.
      NOTIFICATIONS_3D_YAW = 0.0,  // Degrees relative to notifications direction.
      NOTIFICATIONS_3D_PITCH = -60.0,  // Degrees from vertical.
      NOTIFICATION_3D_SCALE = 0.002,  // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions.
      NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE,  // Need a little more room for button in 3D.
      overlay3DDetails = [];
  
  //  push data from above to the 2 dimensional array
  function createArrays(notice, button, createTime, height, myAlpha) {
      arrays.push([notice, button, createTime, height, myAlpha]);
  }
  
  //  This handles the final dismissal of a notification after fading
  function dismiss(firstNoteOut, firstButOut, firstOut) {
      if (firstNoteOut == lodTextID) {
          lodTextID = false;
      }
  
      Overlays.deleteOverlay(firstNoteOut);
      Overlays.deleteOverlay(firstButOut);
      notifications.splice(firstOut, 1);
      buttons.splice(firstOut, 1);
      times.splice(firstOut, 1);
      heights.splice(firstOut, 1);
      myAlpha.splice(firstOut, 1);
      overlay3DDetails.splice(firstOut, 1);
  }
  
  function fadeIn(noticeIn, buttonIn) {
      var q = 0,
          qFade,
          pauseTimer = null;
  
      pauseTimer = Script.setInterval(function () {
          q += 1;
          qFade = q / 10.0;
          Overlays.editOverlay(noticeIn, { alpha: qFade });
          Overlays.editOverlay(buttonIn, { alpha: qFade });
          if (q >= 9.0) {
              Script.clearInterval(pauseTimer);
          }
      }, 10);
  }
  
  //  this fades the notification ready for dismissal, and removes it from the arrays
  function fadeOut(noticeOut, buttonOut, arraysOut) {
      var r = 9.0,
          rFade,
          pauseTimer = null;
  
      pauseTimer = Script.setInterval(function () {
          r -= 1;
          rFade = r / 10.0;
          Overlays.editOverlay(noticeOut, { alpha: rFade });
          Overlays.editOverlay(buttonOut, { alpha: rFade });
          if (r < 0) {
              dismiss(noticeOut, buttonOut, arraysOut);
              arrays.splice(arraysOut, 1);
              ready = true;
              Script.clearInterval(pauseTimer);
          }
      }, 20);
  }
  
  function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) {
      // Calculates overlay positions and orientations in avatar coordinates.
      var noticeY,
          originOffset,
          notificationOrientation,
          notificationPosition,
          buttonPosition;
  
      // Notification plane positions
      noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2;
      notificationPosition = { x: 0, y: noticeY, z: 0 };
      buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 };
  
      // Rotate plane
      notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH,
          NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0);
      notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition);
      buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition);
  
      // Translate plane
      originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0),
          { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE });
      originOffset.y += NOTIFICATIONS_3D_ELEVATION;
      notificationPosition = Vec3.sum(originOffset, notificationPosition);
      buttonPosition = Vec3.sum(originOffset, buttonPosition);
  
      return {
          notificationOrientation: notificationOrientation,
          notificationPosition: notificationPosition,
          buttonPosition: buttonPosition
      };
  }
  
  //  Pushes data to each array and sets up data for 2nd dimension array 
  //  to handle auxiliary data not carried by the overlay class
  //  specifically notification "heights", "times" of creation, and . 
  function notify(notice, button, height) {
      var noticeWidth,
          noticeHeight,
          positions,
          last;
  
      if (isOnHMD) {
          // Calculate 3D values from 2D overlay properties.
  
          noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH;
          noticeHeight = notice.height * NOTIFICATION_3D_SCALE;
  
          notice.size = { x: noticeWidth, y: noticeHeight };
          notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE;
          notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE;
          notice.bottomMargin = 0;
          notice.rightMargin = 0;
          notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE;
          notice.isFacingAvatar = false;
  
          button.url = button.imageURL;
          button.scale = button.width * NOTIFICATION_3D_SCALE;
          button.isFacingAvatar = false;
  
          positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y);
  
          notifications.push((Overlays.addOverlay("text3d", notice)));
          buttons.push((Overlays.addOverlay("image3d", button)));
          overlay3DDetails.push({
              notificationOrientation: positions.notificationOrientation,
              notificationPosition: positions.notificationPosition,
              buttonPosition: positions.buttonPosition,
              width: noticeWidth,
              height: noticeHeight
          });
      } else {
          var notificationText = Overlays.addOverlay("text", notice);
          notifications.push((notificationText));
          buttons.push((Overlays.addOverlay("image", button)));
      }
  
      height = height + 1.0;
      heights.push(height);
      times.push(new Date().getTime() / 1000);
      myAlpha.push(0);
      last = notifications.length - 1;
      createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]);
      fadeIn(notifications[last], buttons[last]);
      return notificationText;
  }
  
  //  This function creates and sizes the overlays
  function createNotification(text, notificationType) {
      var count = (text.match(/\n/g) || []).length,
          breakPoint = 43.0, // length when new line is added
          extraLine = 0,
          breaks = 0,
          height = 40.0,
          stack = 0,
          level,
          noticeProperties,
          bLevel,
          buttonProperties,
          i;
  
      if (text.length >= breakPoint) {
          breaks = count;
      }
      extraLine = breaks * 16.0;
      for (i = 0; i < heights.length; i += 1) {
          stack = stack + heights[i];
      }
  
      level = (stack + 20.0);
      height = height + extraLine;
      noticeProperties = {
          x: overlayLocationX,
          y: level,
          width: width,
          height: height,
          color: textColor,
          backgroundColor: backColor,
          alpha: backgroundAlpha,
          topMargin: topMargin,
          leftMargin: leftMargin,
          font: {size: fontSize},
          text: text
      };
  
      bLevel = level + 12.0;
      buttonProperties = {
          x: buttonLocationX,
          y: bLevel,
          width: 10.0,
          height: 10.0,
          subImage: { x: 0, y: 0, width: 10, height: 10 },
          imageURL: "http://hifi-public.s3.amazonaws.com/images/close-small-light.svg",
          color: { red: 255, green: 255, blue: 255},
          visible: true,
          alpha: backgroundAlpha
      };
  
      if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) &&
          Menu.isOptionChecked(NotificationType.getMenuString(notificationType)))
      {
          randomSounds.playRandom();
      }
  
      return notify(noticeProperties, buttonProperties, height);
  }
  
  function deleteNotification(index) {
      var notificationTextID = notifications[index];
      if (notificationTextID == lodTextID) {
          lodTextID = false;
      }
      Overlays.deleteOverlay(notificationTextID);
      Overlays.deleteOverlay(buttons[index]);
      notifications.splice(index, 1);
      buttons.splice(index, 1);
      times.splice(index, 1);
      heights.splice(index, 1);
      myAlpha.splice(index, 1);
      overlay3DDetails.splice(index, 1);
      arrays.splice(index, 1);
  }
  
  //  wraps whole word to newline
  function stringDivider(str, slotWidth, spaceReplacer) {
      var p,
          left,
          right;
  
      if (str.length > slotWidth) {
          p = slotWidth;
          while (p > 0 && str[p] !== ' ') {
              p -= 1;
          }
  
          if (p > 0) {
              left = str.substring(0, p);
              right = str.substring(p + 1);
              return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer);
          }
      }
      return str;
  }
  
  //  formats string to add newline every 43 chars
  function wordWrap(str) {
      return stringDivider(str, 43.0, "\n");
  }
  
  //  This fires a notification on window resize
  function checkSize() {
      if ((Window.innerWidth !== ourWidth) || (Window.innerHeight !== ourHeight)) {
          var windowResize = "Window has been resized";
          ourWidth = Window.innerWidth;
          ourHeight = Window.innerHeight;
          windowDimensions = Controller.getViewportDimensions();
          overlayLocationX = (windowDimensions.x - (width + 60.0));
          buttonLocationX = overlayLocationX + (width - 35.0);
          createNotification(windowResize, NotificationType.WINDOW_RESIZE);
      }
  }
  
  function update() {
      var nextOverlay,
          noticeOut,
          buttonOut,
          arraysOut,
          defaultEyePosition,
          avatarOrientation,
          notificationPosition,
          notificationOrientation,
          buttonPosition,
          positions,
          i,
          j,
          k;
  
      if (isOnHMD !== HMD.active) {
          while (arrays.length > 0) {
              deleteNotification(0);
          }
          isOnHMD = !isOnHMD;
          persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D;
          return;
      }
  
      frame += 1;
      if ((frame % 60.0) === 0) { // only update once a second
          checkSize(); // checks for size change to trigger windowResize notification
          locationY = 20.0;
          for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade
              nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY });
              Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY });
              Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 });
              if (isOnHMD) {
                  positions = calculate3DOverlayPositions(overlay3DDetails[i].width, overlay3DDetails[i].height, locationY);
                  overlay3DDetails[i].notificationOrientation = positions.notificationOrientation;
                  overlay3DDetails[i].notificationPosition = positions.notificationPosition;
                  overlay3DDetails[i].buttonPosition = positions.buttonPosition;
              }
              locationY = locationY + arrays[i][3];
          }
      }
  
      //  This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1)
      for (i = 0; i < arrays.length; i += 1) {
          if (ready) {
              j = arrays[i][2];
              k = j + persistTime;
              if (k < (new Date().getTime() / 1000)) {
                  ready = false;
                  noticeOut = arrays[i][0];
                  buttonOut = arrays[i][1];
                  arraysOut = i;
                  fadeOut(noticeOut, buttonOut, arraysOut);
              }
          }
      }
  
      if (isOnHMD && notifications.length > 0) {
          // Update 3D overlays to maintain positions relative to avatar
          defaultEyePosition = MyAvatar.getDefaultEyePosition();
          avatarOrientation = MyAvatar.orientation;
  
          for (i = 0; i < notifications.length; i += 1) {
              notificationPosition = Vec3.sum(defaultEyePosition,
                  Vec3.multiplyQbyV(avatarOrientation, overlay3DDetails[i].notificationPosition));
              notificationOrientation = Quat.multiply(avatarOrientation, overlay3DDetails[i].notificationOrientation);
              buttonPosition = Vec3.sum(defaultEyePosition,
                  Vec3.multiplyQbyV(avatarOrientation, overlay3DDetails[i].buttonPosition));
              Overlays.editOverlay(notifications[i], { position: notificationPosition, rotation: notificationOrientation });
              Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation });
          }
      }
  }
  
  var STARTUP_TIMEOUT = 500,  // ms
      startingUp = true,
      startupTimer = null;
  
  function finishStartup() {
      startingUp = false;
      Script.clearTimeout(startupTimer);
  }
  
  function isStartingUp() {
      // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT
      if (startingUp) {
          if (startupTimer) {
              Script.clearTimeout(startupTimer);
          }
          startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT);
      }
      return startingUp;
  }
  
  //  Triggers mic mute notification
  function onMuteStateChanged() {
      var muteState,
          muteString;
  
      muteState = AudioDevice.getMuted() ? "muted" : "unmuted";
      muteString = "Microphone is now " + muteState;
      createNotification(muteString, NotificationType.MUTE_TOGGLE);
  }
  
  function onDomainConnectionRefused(reason) {
      createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED );
  }
  
  //  handles mouse clicks on buttons
  function mousePressEvent(event) {
      var pickRay,
          clickedOverlay,
          i;
  
      if (isOnHMD) {
          pickRay = Camera.computePickRay(event.x, event.y);
          clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID;
      } else {
          clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y });
      }
  
      for (i = 0; i < buttons.length; i += 1) {
          if (clickedOverlay === buttons[i]) {
              deleteNotification(i);
          }
      }
  }
  
  //  Control key remains active only while key is held down
  function keyReleaseEvent(key) {
      if (key.key === 16777249) {
          ctrlIsPressed = false;
      }
  }
  
  //  Triggers notification on specific key driven events
  function keyPressEvent(key) {
      var noteString;
  
      if (key.key === 16777249) {
          ctrlIsPressed = true;
      }
  
      if (key.text === "s") {
          if (ctrlIsPressed === true) {
              noteString = "Snapshot taken.";
              createNotification(noteString, NotificationType.SNAPSHOT);
          }
      }
  }
  
  function setup() {
      Menu.addMenu(MENU_NAME);
      var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING);
      checked = checked === '' ? true : checked;
      Menu.addMenuItem({
          menuName: MENU_NAME,
          menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM,
          isCheckable: true,
          isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING)
      });
      Menu.addSeparator(MENU_NAME, "Play sounds for:");
      for (type in NotificationType.properties) {
          checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1));
          checked = checked === '' ? true : checked;
          Menu.addMenuItem({
              menuName: MENU_NAME,
              menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST,
              isCheckable: true,
              isChecked: checked
          });
      }
  }
  
  //  When our script shuts down, we should clean up all of our overlays
  function scriptEnding() {
      for (var i = 0; i < notifications.length; i++) {
          Overlays.deleteOverlay(notifications[i]);
          Overlays.deleteOverlay(buttons[i]);
      }
      Menu.removeMenu(MENU_NAME);
  }
  
  function menuItemEvent(menuItem) {
      if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) {
          Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM));
          return;
      }
      var notificationType = NotificationType.getTypeFromMenuItem(menuItem);
      if (notificationType !== notificationType.UNKNOWN) {
          Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem));
      }
  }
  
  LODManager.LODDecreased.connect(function() {
      var warningText = "\n"
              + "Due to the complexity of the content, the \n"
              + "level of detail has been decreased. "
              + "You can now see: \n" 
              + LODManager.getLODFeedbackText();
  
      if (lodTextID == false) {
          lodTextID = createNotification(warningText, NotificationType.LOD_WARNING);
      } else {
          Overlays.editOverlay(lodTextID, { text: warningText });
      }
  });
  
  AudioDevice.muteToggled.connect(onMuteStateChanged);
  Controller.keyPressEvent.connect(keyPressEvent);
  Controller.mousePressEvent.connect(mousePressEvent);
  Controller.keyReleaseEvent.connect(keyReleaseEvent);
  Script.update.connect(update);
  Script.scriptEnding.connect(scriptEnding);
  Menu.menuItemEvent.connect(menuItemEvent);
  Window.domainConnectionRefused.connect(onDomainConnectionRefused);
  
  setup();
  