text: /*
   * Windows Display Settings Handler
   *
   * Copyright 2016 Raising the Floor - US
   *
   * Licensed under the New BSD license. You may not use this file except in
   * compliance with this License.
   *
   * You may obtain a copy of the License at
   * https://github.com/GPII/universal/blob/master/LICENSE.txt
   */
  
  "use strict";
  
  var ref = require("ref");
  var Struct = require("ref-struct");
  var arrayType = require("ref-array");
  var ffi = require("ffi-napi");
  var fluid = require("gpii-universal");
  var os = require("os");
  var semver = require("semver");
  
  var gpii = fluid.registerNamespace("gpii");
  var windows = fluid.registerNamespace("gpii.windows");
  
  fluid.registerNamespace("gpii.windows.display");
  fluid.registerNamespace("gpii.windows.displaySettingsHandler");
  
  // The OS specific files can only be loaded from within the OS is it intended for.
  var ver = os.release();
  if (semver.gte(ver, "10.0.0")) {
      // Windows 10
      require("./dpiWindows10.js");
  } else {
      require("./dpiWindows8.js");
  }
  
  var t = windows.types;
  var c = windows.API_constants;
  
  var CCHFORMNAME = 32;
  
  // https://msdn.microsoft.com/en-us/library/windows/desktop/dd183565(v=vs.85).aspx
  windows.DEVMODEW = new Struct([
      [arrayType(t.TCHAR, c.CCHDEVICENAME), "dmDeviceName"],
      [t.WORD, "dmSpecVersion"],
      [t.WORD, "dmDriverVersion"],
      [t.WORD, "dmSize"],
      [t.WORD, "dmDriverExtra"],
      [t.DWORD, "dmFields"],
      ["short", "dmOrientation"],
      ["short", "dmPaperSize"],
      ["short", "dmPaperLength"],
      ["short", "dmPaperWidth"],
      ["short", "dmScale"],
      ["short", "dmCopies"],
      ["short", "dmDefaultSource"],
      ["short", "dmPrintQuality"],
      ["short", "dmColor"],
      ["short", "dmDuplex"],
      ["short", "dmYResolution"],
      ["short", "dmTTOption"],
      ["short", "dmCollate"],
      [arrayType(t.TCHAR, CCHFORMNAME), "dmFormName"],
      [t.WORD, "dmLogPixels"],
      [t.DWORD, "dmBitsPerPel"],
      [t.DWORD, "dmPelsWidth"],
      [t.DWORD, "dmPelsHeight"],
      [t.DWORD, "dmDisplayFlags"],
      [t.DWORD, "dmDisplayFrequency"],
      [t.DWORD, "dmICMMethod"],
      [t.DWORD, "dmICMIntent"],
      [t.DWORD, "dmMediaType"],
      [t.DWORD, "dmDitherType"],
      [t.DWORD, "dmReserved1"],
      [t.DWORD, "dmReserved2"],
      [t.DWORD, "dmPanningWidth"],
      [t.DWORD, "dmPanningHeight"]
  ]);
  
  windows.display.user32 = ffi.Library("user32", {
      // https://msdn.microsoft.com/en-us/library/windows/desktop/dd162611(v=vs.85).aspx
      // LPCWSTR, DWORD, DEVMODE*
      "EnumDisplaySettingsW": [
          t.BOOL, ["pointer", t.DWORD, "pointer"]
      ],
      // https://msdn.microsoft.com/en-us/library/windows/desktop/dd183411(v=vs.85).aspx
      // DEVMODE*, DWORD
      "ChangeDisplaySettingsW": [
          t.LONG, ["pointer", t.DWORD]
      ]
  });
  
  /*
   * Creates and initializes a new DEVMODEW structure
   */
  windows.display.createDevModeStruct = function () {
      var dm = new windows.DEVMODEW();
      dm.ref().fill(0);
      dm.dmSize = windows.DEVMODEW.size;
      return dm;
  };
  
  /*
   * Returns if a screen resolution is invalid
   *
   * @param (Object)
   * @returns {Boolean} true if invalid
   */
  windows.display.isInvalid = function (screenRes) {
      var isInvalid = true;
      if (typeof(screenRes.width) === "number" && typeof(screenRes.height) === "number" && screenRes.width > 0 && screenRes.height > 0) {
          fluid.each(windows.display.getAvailableResolutions(), function (validScreenRes) {
              if (validScreenRes.dmPelsWidth === screenRes.width && validScreenRes.dmPelsHeight === screenRes.height) {
                  isInvalid = false;
              };
          });
      }
      return isInvalid;
  };
  
  /*
   * Gets the current display's screen resolution
   *
   * @return {Object) The width and height of the screen.
   */
  windows.display.getScreenResolution = function () {
      var dm = windows.display.createDevModeStruct();
      if (windows.display.user32.EnumDisplaySettingsW(ref.NULL, c.ENUM_CURRENT_SETTINGS, dm.ref()) !== c.FALSE) {
          // note for unknown reason on win 10 the returned dmSize is 188 not expected 220
          var closestScreenRes = windows.display.findClosestResolution({width: dm.dmPelsWidth, height: dm.dmPelsHeight});
          return { width: closestScreenRes.dmPelsWidth, height: closestScreenRes.dmPelsHeight };
      } else {
          fluid.fail("Couldn't retrieve the current screen resolution");
      }
  };
  
  /*
   * Gets available resolutions
   *
   * @return {list} of resolutions
   */
  windows.display.getAvailableResolutions = function () {
      var index = 0;
      var dm = windows.display.createDevModeStruct();
      var availableResolutions = [];
  
      while (windows.display.user32.EnumDisplaySettingsW(ref.NULL, index++, dm.ref()) !== c.FALSE) {
          var curr = {
              dmPelsWidth: dm.dmPelsWidth,
              dmPelsHeight: dm.dmPelsHeight
          };
          availableResolutions.push(curr);
      };
  
      fluid.log("windows.display.getAvailableResolutions got available screen resolutions ", availableResolutions);
  
      return availableResolutions;
  };
  
  /*
   * Sets the current display's screen resolution if possible
   *
   * @param {Object} The new screen resolution width and height
   * @return {Boolean} true if successful
   */
  windows.display.setScreenResolution = function (newRes) {
      if (windows.display.isInvalid(newRes)) {
          fluid.fail("Received an invalid screen resolution: ", newRes);
      } else {
          var dmCurrent = windows.display.createDevModeStruct();
          if (windows.display.user32.EnumDisplaySettingsW(ref.NULL, c.ENUM_CURRENT_SETTINGS, dmCurrent.ref()) !== c.FALSE) {
              fluid.log("windows.display.setScreenResolution about to set resolution ", newRes);
              // Note: ChangeDisplaySettings has no effect if new is same as current
              dmCurrent.dmPelsWidth = newRes.width;
              dmCurrent.dmPelsHeight = newRes.height;
              var lRet = windows.display.user32.ChangeDisplaySettingsW(dmCurrent.ref(), 0);
              if (lRet === c.DISP_CHANGE_SUCCESSFUL) {
                  return true;
              } else if (lRet === c.DISP_CHANGE_RESTART) {
                  // TODO: According to the returned code, the computer must be restarted for
                  // the graphics mode to work. For now we return 'true', but this special case
                  // is related to https://issues.gpii.net/browse/GPII-1982, and we must consider
                  // this as something that needs to be taken into account and even notified to
                  // the user.
                  //
                  return true;
              } else {
                  // Return codes explained at https://msdn.microsoft.com/en-us/library/dd183411.aspx
                  fluid.fail("Couldn't set screen resolution, return code was " + lRet);
              }
          }
      }
  };
  
  /**
   * Gets the size of the desktop in logical pixels, taking the current DPI setting into consideration.
   * This is different to the screen resolution, which counts the physical pixels.
   *
   * @return {Object} width and height of the desktop.
   */
  windows.display.getDesktopSize = function () {
      return {
          width: windows.user32.GetSystemMetrics(windows.API_constants.SM_CXSCREEN),
          height: windows.user32.GetSystemMetrics(windows.API_constants.SM_CYSCREEN)
      };
  };
  
  windows.display.setImpl = function (payload) {
      var results = {};
  
      // Changing the screen resolution or DPI moves the mouse pointer to the centre of the screen, so get the current
      // position and restore it after the setting change.
      var cursorPos = new windows.POINT();
      windows.user32.GetCursorPos(cursorPos.ref());
      var origSize = windows.display.getDesktopSize();
  
      var targetRes = payload.settings["screen-resolution"];
      if (targetRes) {
          var oldRes = windows.display.getScreenResolution();
          var newRes = oldRes;
  
          if (typeof(targetRes.width) !== "number" || typeof(targetRes.height) !== "number") {
              fluid.fail("Incorrect payload for screen resolution: " +
                  JSON.stringify(payload, null, 4));
          }
          else if (windows.display.setScreenResolution(targetRes)) {
              newRes = targetRes;
          }
  
          results["screen-resolution"] = { oldValue: oldRes, newValue: newRes };
      }
  
      var targetDpi = payload.settings["screen-dpi"];
      if (Number.isInteger(targetDpi)) {
          var oldDpi = windows.display.getScreenDpi();
          windows.display.setScreenDpi(targetDpi);
          results["screen-dpi"] = { oldValue: oldDpi.configured, newValue: targetDpi };
      }
  
      fluid.log("display settings handler SET returning results ", results);
  
      // Restore the mouse position, taking into account the change in screen size, so it is back to the previous physical
      // position.
      var newSize = windows.display.getDesktopSize();
      var x = cursorPos.x * (newSize.width / origSize.width);
      var y = cursorPos.y * (newSize.height / origSize.height);
      windows.user32.SetCursorPos(x, y);
  
      return results;
  };
  
  windows.display.getImpl = function () {
      var curRes = windows.display.getScreenResolution();
      var curDpi = windows.display.getScreenDpi();
  
      var results = {
          "screen-resolution": curRes,
          "screen-dpi": curDpi.configured
      };
  
      return results;
  };
  
  windows.displaySettingsHandler.get = function (payload) {
      return gpii.settingsHandlers.invokeSettingsHandler(windows.display.getImpl, payload);
  };
  
  windows.displaySettingsHandler.set = function (payload) {
      return gpii.settingsHandlers.invokeSettingsHandler(windows.display.setImpl, payload);
  };
  
  // Internal utilities
  // TODO: Move this into gpii.settingsHandlerUtilities and add tests
  
  windows.display.calculateDistance = function (x, y) {
      var a = x[0] - y[0];
      var b = x[1] - y[1];
      return Math.sqrt(a * a + b * b);
  };
  
  windows.display.findClosestResolution = function (res) {
      var distance = null;
      var closest = null;
  
      var availableResolutions = windows.display.getAvailableResolutions();
  
      fluid.each(availableResolutions, function (currRes) {
          var currDist = windows.display.calculateDistance([res.width, res.height], [currRes.dmPelsWidth, currRes.dmPelsHeight]);
          if (distance === null || currDist < distance) {
              distance = currDist;
              closest = currRes;
          }
      });
      return closest;
  };
  